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's 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 {background:[[ColorPalette::PrimaryPale]];}
.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 { 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]];}

.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;}

.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 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;}
.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
/***
|Description|Allows to "cook" tiddlers using various "recipes" – automatically (once a "part" is updated) and using a button|
|Version|0.2.5|
|Author|Yakov Litvin|
|Source|not released yet|
***/

//{{{
// creates a hidden section with lines commented by "//"
// to prevent css comments from being treated as end of js comments;
// adds js that grabs that section (of nameOfTiddlerToCook tiddler), removes "//"
// in each line and creates a shadow tiddler treated as css (..)
function wrapAsCssAdder(css, nameOfTiddlerToCook, cssName, options) {

	return	'// /%\n' +
		'/***\n!' + cssName + '\n***/\n' +
		css.replace(/^/gm, '//') +
		'\n/***\n!end of '+ cssName + '\n***/\n'+
		'// %/ //\n' +
		'//{{{\n'+
		';(function() {\n' +
		'var cssName = ' + JSON.stringify(cssName) + ',\n' +
		'    css = store.getTiddlerText(' + JSON.stringify(nameOfTiddlerToCook) + ' + \"##\" + cssName).replace(/^\\/\\//gm, \"\");\n' +
		'css = css.substring(5, css.length - 5); // cut leading \\n***/ and trailing /***\\n of the section\n' +
		'config.shadowTiddlers[cssName] = css;\n' +
		(options && options.dontApply ? '' :
			'store.addNotification(cssName, refreshStyles);\n' +
			'store.addNotification("ColorPalette", function(smth, doc) { refreshStyles(cssName, doc) })\n'
		) +
		'})();\n' +
		'//}}}'
}

config.extensions.cookBook = {

	// hashmap by tiddlerName of
	// - parts: Array of tiddlerTextExpr (tName, tName::slNmae, tName##seName, ::slNmae,)
	// - steps: JavaScript expression containing parts[i] to be evaled that returns text,
	//          null means join("\n") of parts[i] ~contents
	// - title: equal to tiddlerName for usage flexibility
	recipes: {},
	addRecipe: function(title, parts, steps, tags, autoupdate, reinstall, minify, reinstallOnly) {

		this.recipes[title] = { title: title, parts: parts, steps: steps, tags: tags, autoupdate: autoupdate, reinstall: reinstall, reinstallOnly: reinstallOnly }
	},
	getRecipeFor: function(title) { return this.recipes[title] },
	getRecipesContaining: function(title) {

		var goodPartRegExp = new RegExp("^" + title.escapeRegExp() + "(?:$|::.+|##.+)")
		var demandedRecipes = [], t, recipeParts, i;
		for(t in this.recipes) {
			recipeParts = this.recipes[t].parts;
			for(i = 0; i < recipeParts.length; i++)
				if(goodPartRegExp.exec(recipeParts[i])) {
					demandedRecipes.push(this.recipes[t]);
					break;
				}
		}
		return demandedRecipes;
	}
};

config.macros.defineRecipe = {

	readRecipeList: function() {

		if (!window.store) return setTimeout(readRecipeList, 100);

		var recipeList = store.fetchTiddler("RecipeList");
		if(recipeList && recipeList.text)
			wikify(recipeList.text, document.createElement("div"), null, recipeList);
	},
	handler: function(place, macroName, params, wikifier, paramString, tiddler) {

		var pParams = paramString.parseParams("tiddler", null, true, false, true),
		    checkFlag	 = function(name, default_val) {
			return getParam(pParams, name, default_val) ||
				!!((new RegExp("\\s"+name+"(?:\\s|$)")).exec(paramString))
		    },
		    tiddlerNames = pParams[0]["tiddler"],
		    partNames	 = pParams[0]["parts"],
		    tags	 = pParams[0]["tags"] || [],
		    plugin	 = checkFlag("plugin"),
		    recipe	 = getParam(pParams, "recipe", ""),
		    autoupdate	 = checkFlag("autoupdate"),
		    reinstallOnly= checkFlag("reinstallOnly"),
		    reinstall	 = checkFlag("reinstall") || reinstallOnly,
		    minify	 = checkFlag("minify");
		if(plugin) tags.push("systemConfig");

		if(!tiddlerNames) {
			createTiddlyError(place,
			  "Macro error: no 'tiddler' param (click this for details)",
			  "Put it after the macro name as either 'tiddler:\"tiddlerName\"' or just '\"tiddlerName\"'");
			return;
		}
		if(reinstallOnly) partNames = tiddlerNames;
		if(!partNames) {
			createTiddlyError(place,"Macro error: no 'parts' param (click this for details)","Put them after the 'tiddler' param as 'parts:\"partName1\" \"partName2\" ...'");
			return;
		}
		var tiddlerName = tiddlerNames[0],
		    sameTiddlerPartRegExp = /^(::|##).+/, i;
		for(i = 0; i < partNames.length; i++)
			if(sameTiddlerPartRegExp.exec(partNames[i]))
				partNames[i] = tiddlerName + partNames[i];

		// show the written macro code:
		var w = wikifier,
		    macroTWcode = w.source.substring(w.matchStart, w.nextMatch),
		    hide = params.contains('hide');
		if (!hide)
			createTiddlyText(createTiddlyElement(place, "pre"), macroTWcode);

		config.extensions.cookBook.addRecipe(tiddlerName, partNames,
			recipe, tags, autoupdate, reinstall, minify, reinstallOnly);
	}
};

config.macros.cook = {

	cookRecipe: function(recipe, force) {

		if(!force && !recipe.autoupdate) return;

		var partNames = recipe.parts, parts = {}, i;
		for(i = 0; i < partNames.length; i++)
			parts[partNames[i]] = [ store.getTiddlerText(partNames[i], "") ];
//# for filtering, add extra parsing here (fill the array with several parts)

		var script = "config.macros.cook.f = function(title, partNames, parts) {" +
			"var text = '', tid = new Tiddler(title)," +

			"    oldTid  = store.fetchTiddler(title);" +
			"tid.creator = oldTid ? oldTid.creator : config.options.txtUserName;" +
			"tid.created = oldTid ? oldTid.created : new Date();" +
			"tid.modifier = config.options.txtUserName;" +
			"tid.modified = new Date();" +
			"var oldChangeCount = oldTid ? oldTid.fields.changecount : null;" +
			"tid.fields.changecount = oldChangeCount ? (parseInt(oldChangeCount)+1) : 0;\n" +

			recipe.steps +

			"\nvar partName, namedParts, i;" +
			"for(partName in parts) {" +
			"    namedParts = parts[partName];" +
			"    for(i = 0; i < namedParts.length; i++)" +
			"        text += (namedParts[i]+'\\n');" +
			"};" +
			"if(text) text = text.substr(0,text.length-1);" +
			"tid.text = tid.text || text;" +
			"return tid;" +
		"};"
console.log("in cookRecipe, script is",script);
		eval(script);

		var tid      = this.f(recipe.title, partNames, parts),
		    modifier = config.options.txtUserName,
		    modified = new Date();
		tid.tags = tid.tags.concat(recipe.tags);
		if(recipe.minify) {
			tid.text = tid.text.replace(/\r/gm,'\n');
			//# implement minification here, use some external lib
		}
		
console.log("in cookRecipe, tid is",tid);
		if(!recipe.reinstallOnly) {
			store.saveTiddler(tid, recipe.title, null, modifier, modified);
			displayMessage("Tiddler \""+ recipe.title +"\" has been cooked");
console.log("Tiddler \""+recipe.title+"\" has been cooked");
//# add msg that tells the results (test this with both ways of cooking)
//# add msg that tells if some part wasn't found
		}
		if(recipe.reinstall /*&& isPluginEnabled(tid)*/) {
			//# do stuff from STP's installPlugin (logging)
			var msg;
			try {
				window.eval(tid.text);
				msg = '"'+ tid.title +'" was reinstalled.';
			} catch(ex) {
				// do react on errors in a helpful way
				msg = "Error evaluating "+ tid.title +":\n"+
					  exceptionText(ex)
			} finally {
				displayMessage(msg);
				console.log(msg);
			}
			//# do stuff from STP's installPlugin (logging)
		}
	},
	cookRecipeFor: function(title, force) {

		var recipe = config.extensions.cookBook.getRecipeFor(title);
		if(recipe)
			this.cookRecipe(recipe, force);
		else
			return 'no recipe for "'+ title +'"';
	},
	cookRecipesContaining: function(title, force) {

		var recipes = config.extensions.cookBook.getRecipesContaining(title), i;
		for(i = 0; i < recipes.length; i++)
			this.cookRecipe(recipes[i], force);
	},
	handler: function(place, macroName, params, wikifier, paramString, tiddler) {

		var pParams = paramString.parseParams("title", null, true, false, true),
		    title = getParam(pParams, "title", tiddler ? tiddler.title : ""),
		    label = getParam(pParams, "label", "cook"),
		    tooltip = getParam(pParams, "tooltip", "");
		
		createTiddlyButton(place, label, tooltip, function() {
			var error = config.macros.cook.cookRecipeFor(title, true);
			if(error) displayMessage(error);
		});
	}
};

// hijack in a reinstallable fashion
if(!config.commands.saveTiddler.orig_handler_CTP)
 config.commands.saveTiddler.orig_handler_CTP = config.commands.saveTiddler.handler;
config.commands.saveTiddler.handler = function(event, src, title) {

	// "pre-saving" of the tiddler's text for cooking to see it
	// (code extracted from Story.saveTiddler)
	var tiddlerElem = story.getTiddler(title);
	if(tiddlerElem) {
		var fields = {};
		story.gatherSaveFields(tiddlerElem, fields);
		var tiddler = store.saveTiddler(title, title, fields.text);
	}

	config.macros.cook.cookRecipesContaining(title);
//# add the corresponding messages

	return config.commands.saveTiddler.orig_handler_CTP.apply(this, arguments);
	// it's important not to use "this" here (causes conflicts with CodeMirror and may do so with others)
};

setTimeout(config.macros.defineRecipe.readRecipeList, 100);
//}}}
/***
|''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/|
|''Author''|Yakov Litvin|
|''Version''|1.3.2|
|''Source''|https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js|
|''License''|[[MIT|https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/LICENSE]]|
!!!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)
	}
}

// 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
!!!
***/
|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.1.1|
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_Extensions/blob/master/TranslationsCollection.txt",
    "description": "TiddlyWiki Translations",
    "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"
  }
]
//}}}
/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version    |0.6.5|
|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 = /\/(\w+)\.(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 {
			return JSON.parse(match[2])
		} catch (e) {
			console.log(`problems with parsing ${centralSourcesListName}:`, e)
			return null
		}
	},
	//# use getAvailableRepositories to get lists of extensions
	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)
			}
		}

		//# move name normalizing to the reading method
		//  once we move the list of available extensions from hardcode
		for(const extension of availableExtensions) {
			extension.name = extension.name || this.guessNameByUrl(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)
				tiddler.text = responseText
				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: '<<extensionsExplorer>>',
		}
	},
	handler: function(place, macroName, params, wikifier, paramString) {
		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>')

		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()

		// 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
		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?
		$tbody.append(jQuery(`<tr><td colspan="4" style="text-align: center;">Installed</td></tr>`))
		for(const extensionTiddler of installedExtensionsTiddlers) {
			//# limit the width of the Description column/whole table
			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[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) })
}
//}}}
ResponsiveThemePlugin is [[cooked|RecipeList]] from +++[components]
* ResponsiveThemeSkeleton
** extending MarkupPreHead (pre-head is built on the next save, used on reload)
* ResponsivePageTemplate
* (no custom ViewTemplate or EditTemplate yet)
* ResponsiveStyleSheet
* TopLineMenu + TopLineMenuMiddle
=== +++[Some of finished tasks]
* (+) create "central column", colorize outer part
* (+) remake main menu using flex, add classes that make Holy Grail CSS easy to understand
* (+) set min height of #contentWrapper to 100vh
* (+) make backstage hover over header, avoid shifting content down
* (+) get rid of .headerShadow and gradient macro (see core)
* (+) add viewport
===; Other todos:
* (+) pack backstage elements (#backstageButton, #backstageArea, #backstage, but not #backstageCloak – should be full width) into a single container ([[via JS|ResponsiveThemeSkeleton]])
** (+) fix: now clock is over the whole backstage (see [[here|https://stackoverflow.com/q/31747334/3995261]]; [[here|https://css-tricks.com/full-bleed/]])
*** not going to solve the page horizontal scrollbar on backstage open soon
** (+) put that container inside a common #fullContentWrapper (#contentWrapper in there, too), position #messageArea relatively to the right edge of #fullContentWrapper
*** ok: now #messageArea is shown outside the #fullContentWrapper
*** may be do the same in the core
** (+) #backstageCloak shouldn't add vertical scroll when the content doesn't overflow
** (+) limit container width, like max-width: 80em;
*** todo: make optional
* (+) +++[pack all components]
*  (+) use ResponsiveThemeSkeleton, ResponsivePageTemplate, and RecipeList
*  (+) pack ResponsiveStyleSheet via tweaked {{{wrapAsCssAdder}}}, clean StyleSheet
**  (+) make StyleSheet updates (for custom styles) applied at once, like usually
*  (+) pack populating MarkupPreHead
*  (+) fix: in 2.9.2, messages' styles are not applied (add {{{class="messageArea"}}})
*  (+) fix: Source slice value is displayed partially (text before {{{;}}} is parsed as CSS)
=== into ResponsiveThemePlugin
** (+) pack TopLineMenu + TopLineMenuMiddle
*** (+) using a hack: reuse {{{wrapAsCssAdder}}}, back-propagate the "4 → 5" fix
*** update CookTiddlerPlugin: should have a separate ~{{{wrapAsShadow}}} helper
* +++[hide message on click]
review what I'm using on my mobile nodes:
//{{{
// TODO: should be click outside #messageArea
jQuery(document.body).bind("click", function() {
	clearMessage()
	return true
})
// is this to avoid scenarios when a click on saveChanges closes the message generated by saving?
if(!config.extensions.postponeMsg) {
	config.extensions.postponeMsg = true
	var orig_displayMessage = displayMessage
	displayMessage = function(a, b) {
		// TODO: use .apply instead?
		setTimeout(() => orig_displayMessage(a, b), 100)
	}
}
//}}}
=== 
* note: TiddlersBarPlugin requires editing ResponsivePageTemplate
* [jumper: besides viewport and other responsive, make sure it stays in the middle vertically, like on desktop (reproduced on desktop in dev tools in the [[repo|https://yakovl.github.io/TiddlyWiki_JumpKeysPlugin/]])]
* remake header + main m. + main + sidebar using grid, check backstage and messages
* MainMenu, ''sidebar'':
** (+) hide on narrow screens (iteration 1)
** (+) add a sticky TopLineMenu (via PageTemplate), buttons to open the menus
*** ''try to fix'': on ctrl + home in an edited tiddler, the cursor may get invisible as the sticky menu hovers over it, plus doesn't play well with jumping
** (+) make the middle content transcluded from another tiddler (TopLineMenuMiddle)
*** (+) fix: TLMM content is only shown +++[after editing TLMM] but not on startup, and refreshing (like on clicking DMP button) hides it === 
** (+) add closing of menus on click elsewhere when they're hovering
** make the paddings consistent (same as the content, which may be reduced)
** fix: lists in sidebar shouldn't get outside the main container in the mobile mode
** optionally: remember state in options (so that one can hide MainMenu permanently)
** consider showing both menus as ~sticky (to the top), with a scrollbar: auto to main and sidebar lists
** consider adding backstage button in the topline as well; save button? other elements from sidebar?
** later: animate toggling
** alternatives: sticky menu floating on the left/right/both (takes less space, avoids the hover over cursor problem)
* add a button/option to toggle header? to toggle full width? to adjust font size? move backstage button into top line? (TopLineMenuMiddle can be used for that, as well as for dark mode switch)
* fix: on mobile, when no tiddlers are open, there are shadows in unexpected positions
* may be worth noting in docs: [[orientation quirk|https://groups.google.com/g/tiddlywikiclassic/c/5ZQYT_-mOS8 on iPhone]] may be solved with {{{body { -webkit-text-size-adjust: none ; } }}}

Design considerations:
* +++[breakpoints]
*  bootstrap uses X-small <576px, Small <768px, Medium <992px, (992px≤ Large <1200px, Extra large ≥1200px, Extra extra large ≥1400px)
*  let's try <768px for mobile: `@media (max-width: 768px) {`
=== 
* font size: _
** (+) for .siteTitle, decrease down to 1.5em
** main: need larger for pm (and pn?!)
* page header: _
* tiddler subtitle: _
* backstage: _
* tiddler toolbar: _
* tags, tagging:
** hide "no tags" (test with intellitagger: the edit button may be shown)
** move to the tiddler subtitle?
* some nice references:
** well-readable: https://vas3k.blog/notes/moderation/, https://vk.com/@yakov_litvin-strannyi-vishlist-2020, Vivaldi read mode, may be FCC
* remove background color from header and margins?
* combining with other themes: _

Delivery:
* (+) [[pre-release|https://groups.google.com/g/tiddlywikiclassic/c/ZkELTvfb6hA]]
* ''add to ~GitHub and EEP collection (or make EEP work with Tiddlyhost)''; probably host via GH pages and redirect from here
** if keeping this as the main demo – how to sync with GH? can TIFP be used for that?
* announce as a full release, ''link the thread here'' (in MainMenu)
* my personal/pre-releases collection; personal nodes + sync (d.eco, studies!)
* make "Hub listed"; turn into a Tiddlyhost template?
* add to classic.tiddlywiki.com? to other sites? (probably to the plugin repos)
* add some bits to the core? starting with viewport?

Infra:
* (+) upgrade to 2.10.1
** make a backup (and facilitate for TH)
** facilitate upgrading @TH
* (+) save [[options|SystemSettings]] (username, no backup, autosave, insert tabs)
** (+) make saving work [[properly on Tiddlyhost|ThostGoodSavingPlugin]] (saving indicator, QSP)
* (+) EEP, central, my collection, DMP
/***
|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.0|
|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 {
			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

	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)
}
//}}}
/***
!!!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]];
}
//}}}
!!!
***/
[[GettingStarted]]

~TiddlyWiki v<<version>>
<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--}}}-->
/***
|Name|NestedSlidersPlugin|
|Source|http://yakovlitvin.pro/TW/pre-releases/NestedSlidersPlugin_patched.txt|
|Documentation|http://TiddlyTools.com/Classic/#NestedSlidersPluginInfo|
|Version|2.4.10|
|Tweaks|codestyle and meta only, dropped support of TW 2.1|
|Author|Yakov Litvin|
|Original Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.2|
|Type|plugin|
|Description|show content in nest-able sliding/floating panels, without creating separate tiddlers for each panel's content|
!!!!!Configuration
<<<
<<option chkFloatingSlidersAnimate>> allow floating sliders to animate when opening/closing
Note: for floating slider animation to occur you must also allow animation in general (see [[AdvancedOptions]]).
<<<
!!!!!Code
***/
//{{{
version.extensions.NestedSlidersPlugin= {major: 2, minor: 4, revision: 9, date: new Date(2008,11,15)};

// options for deferred rendering of sliders that are not initially displayed
if (config.options.chkFloatingSlidersAnimate===undefined)
	config.options.chkFloatingSlidersAnimate=false; // avoid clipping problems in IE

// default styles for 'floating' class
setStylesheet(".floatingPanel { position:absolute; z-index:10; padding:0.5em; margin:0em; \
	background-color:#eee; color:#000; border:1px solid #000; text-align:left; }","floatingPanelStylesheet");

// if removeCookie() function is not defined by TW core, define it here.
if (window.removeCookie===undefined) {
	window.removeCookie=function(name) {
		document.cookie = name+'=; expires=Thu, 01-Jan-1970 00:00:01 UTC; path=/;'; 
	}
}

config.formatters.push( {
	name: "nestedSliders",
	match: "\\n?\\+{3}",
	terminator: "\\s*\\={3}\\n?",
	lookahead: "\\n?\\+{3}(\\+)?(\\([^\\)]*\\))?(\\!*)?(\\^(?:[^\\^\\*\\@\\[\\>]*\\^)?)?(\\*)?(\\@)?(?:\\{\\{([\\w]+[\\s\\w]*)\\{)?(\\[[^\\]]*\\])?(\\[[^\\]]*\\])?(?:\\}{3})?(\\#[^:]*\\:)?(\\>)?(\\.\\.\\.)?\\s*",
	handler: function(w)
		{
			lookaheadRegExp = new RegExp(this.lookahead,"mg");
			lookaheadRegExp.lastIndex = w.matchStart;
			var lookaheadMatch = lookaheadRegExp.exec(w.source)
			if(lookaheadMatch && lookaheadMatch.index == w.matchStart)
			{
				var defopen=lookaheadMatch[1];
				var cookiename=lookaheadMatch[2];
				var header=lookaheadMatch[3];
				var panelwidth=lookaheadMatch[4];
				var transient=lookaheadMatch[5];
				var hover=lookaheadMatch[6];
				var buttonClass=lookaheadMatch[7];
				var label=lookaheadMatch[8];
				var openlabel=lookaheadMatch[9];
				var panelID=lookaheadMatch[10];
				var blockquote=lookaheadMatch[11];
				var deferred=lookaheadMatch[12];

				// location for rendering button and panel
				var place=w.output;

				// default to closed, no cookie, no accesskey, no alternate text/tip
				var show="none"; var cookie=""; var key="";
				var closedtext=">"; var closedtip="";
				var openedtext="<"; var openedtip="";

				// extra "+", default to open
				if (defopen) show="block";

				// cookie, use saved open/closed state
				if (cookiename) {
					cookie=cookiename.trim().slice(1,-1);
					cookie="chkSlider"+cookie;
					if (config.options[cookie]==undefined)
						{ config.options[cookie] = (show=="block") }
					show=config.options[cookie]?"block":"none";
				}

				// parse label/tooltip/accesskey: [label=X|tooltip]
				if (label) {
					var parts=label.trim().slice(1,-1).split("|");
					closedtext=parts.shift();
					if (closedtext.substr(closedtext.length-2,1)=="=")	
						{ key=closedtext.substr(closedtext.length-1,1); closedtext=closedtext.slice(0,-2); }
					openedtext=closedtext;
					if (parts.length) closedtip=openedtip=parts.join("|");
					else { closedtip="show "+closedtext; openedtip="hide "+closedtext; }
				}

				// parse alternate label/tooltip: [label|tooltip]
				if (openlabel) {
					var parts=openlabel.trim().slice(1,-1).split("|");
					openedtext=parts.shift();
					if (parts.length) openedtip=parts.join("|");
					else openedtip="hide "+openedtext;
				}

				var title=show=='block'?openedtext:closedtext;
				var tooltip=show=='block'?openedtip:closedtip;

				// create the button
				if (header) { // use "Hn" header format instead of button/link
					var lvl=(header.length>5)?5:header.length;
					var btn = createTiddlyElement(createTiddlyElement(place,"h"+lvl,null,null,null),"a",null,buttonClass,title);
					btn.onclick=onClickNestedSlider;
					btn.setAttribute("href","javascript:;");
					btn.setAttribute("title",tooltip);
				}
				else
					var btn = createTiddlyButton(place,title,tooltip,onClickNestedSlider,buttonClass);
				btn.innerHTML=title; // enables use of HTML entities in label

				// set extra button attributes
				btn.setAttribute("closedtext",closedtext);
				btn.setAttribute("closedtip",closedtip);
				btn.setAttribute("openedtext",openedtext);
				btn.setAttribute("openedtip",openedtip);
				btn.sliderCookie = cookie; // save the cookiename (if any) in the button object
				btn.defOpen=defopen!=null; // save default open/closed state (boolean)
				btn.keyparam=key; // save the access key letter ("" if none)
				if (key.length) {
					btn.setAttribute("accessKey",key); // init access key
					btn.onfocus=function(){this.setAttribute("accessKey",this.keyparam);}; // **reclaim** access key on focus
				}
				btn.setAttribute("hover",hover?"true":"false");
				btn.onmouseover=function(ev) {
					// optional 'open on hover' handling
					if (this.getAttribute("hover")=="true" && this.sliderPanel.style.display=='none') {
						document.onclick.call(document,ev); // close transients
						onClickNestedSlider(ev); // open this slider
					}
					// mouseover on button aligns floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this,this.sliderPanel);
				}

				// create slider panel
				var panelClass=panelwidth?"floatingPanel":"sliderPanel";
				if (panelID) panelID=panelID.slice(1,-1); // trim off delimiters
				var panel=createTiddlyElement(place,"div",panelID,panelClass,null);
				panel.button = btn; // so the slider panel know which button it belongs to
				btn.sliderPanel=panel; // so the button knows which slider panel it belongs to
				panel.defaultPanelWidth=(panelwidth && panelwidth.length>2)?panelwidth.slice(1,-1):"";
				panel.setAttribute("transient",transient=="*"?"true":"false");
				panel.style.display = show;
				panel.style.width=panel.defaultPanelWidth;
				panel.onmouseover=function(event) // mouseover on panel aligns floater position with button
					{ if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this.button,this); }

				// render slider (or defer until shown) 
				w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
				if ((show=="block")||!deferred) {
					// render now if panel is supposed to be shown or NOT deferred rendering
					w.subWikify(blockquote?createTiddlyElement(panel,"blockquote"):panel,this.terminator);
					// align floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(place,btn,panel);
				}
				else {
					var src = w.source.substr(w.nextMatch);
					var endpos=findMatchingDelimiter(src,"+++","===");
					panel.setAttribute("raw",src.substr(0,endpos));
					panel.setAttribute("blockquote",blockquote?"true":"false");
					panel.setAttribute("rendered","false");
					w.nextMatch += endpos+3;
					if (w.source.substr(w.nextMatch,1)=="\n") w.nextMatch++;
				}
			}
		}
	}
)

function findMatchingDelimiter(src,starttext,endtext) {
	var startpos = 0;
	var endpos = src.indexOf(endtext);
	// check for nested delimiters
	while (src.substring(startpos,endpos-1).indexOf(starttext)!=-1) {
		// count number of nested 'starts'
		var startcount=0;
		var temp = src.substring(startpos,endpos-1);
		var pos=temp.indexOf(starttext);
		while (pos!=-1)  { startcount++; pos=temp.indexOf(starttext,pos+starttext.length); }
		// set up to check for additional 'starts' after adjusting endpos
		startpos=endpos+endtext.length;
		// find endpos for corresponding number of matching 'ends'
		while (startcount && endpos!=-1) {
			endpos = src.indexOf(endtext,endpos+endtext.length);
			startcount--;
		}
	}
	return (endpos==-1)?src.length:endpos;
}
//}}}
//{{{
window.onClickNestedSlider=function(e)
{
	if (!e) var e = window.event;
	var theTarget = resolveTarget(e);
	while (theTarget && theTarget.sliderPanel==undefined) theTarget=theTarget.parentNode;
	if (!theTarget) return false;
	var theSlider = theTarget.sliderPanel;
	var isOpen = theSlider.style.display!="none";

	// if SHIFT-CLICK, dock panel first (see [[MoveablePanelPlugin]])
	if (e.shiftKey && config.macros.moveablePanel) config.macros.moveablePanel.dock(theSlider,e);

	// toggle label
	theTarget.innerHTML=isOpen?theTarget.getAttribute("closedText"):theTarget.getAttribute("openedText");
	// toggle tooltip
	theTarget.setAttribute("title",isOpen?theTarget.getAttribute("closedTip"):theTarget.getAttribute("openedTip"));

	// deferred rendering (if needed)
	if (theSlider.getAttribute("rendered")=="false") {
		var place=theSlider;
		if (theSlider.getAttribute("blockquote")=="true")
			place=createTiddlyElement(place,"blockquote");
		wikify(theSlider.getAttribute("raw"),place);
		theSlider.setAttribute("rendered","true");
	}

	// show/hide the slider
	if(config.options.chkAnimate && (!hasClass(theSlider,'floatingPanel') || config.options.chkFloatingSlidersAnimate))
		anim.startAnimating(new Slider(theSlider,!isOpen,e.shiftKey || e.altKey,"none"));
	else
		theSlider.style.display = isOpen ? "none" : "block";

	// reset to default width (might have been changed via plugin code)
	theSlider.style.width=theSlider.defaultPanelWidth;

	// align floater panel position with target button
	if (!isOpen && window.adjustSliderPos) window.adjustSliderPos(theSlider.parentNode,theTarget,theSlider);

	// if showing panel, set focus to first 'focus-able' element in panel
	if (theSlider.style.display!="none") {
		var ctrls=theSlider.getElementsByTagName("*");
		for (var c=0; c<ctrls.length; c++) {
			var t=ctrls[c].tagName.toLowerCase();
			if ((t=="input" && ctrls[c].type!="hidden") || t=="textarea" || t=="select")
				{ try{ ctrls[c].focus(); } catch(err){;} break; }
		}
	}
	var cookie=theTarget.sliderCookie;
	if (cookie && cookie.length) {
		config.options[cookie]=!isOpen;
		if (config.options[cookie]!=theTarget.defOpen) window.saveOptionCookie(cookie);
		else window.removeCookie(cookie); // remove cookie if slider is in default display state
	}

	// prevent SHIFT-CLICK from being processed by browser (opens blank window... yuck!)
	// prevent clicks *within* a slider button from being processed by browser
	// but allow plain click to bubble up to page background (to close transients, if any)
	if (e.shiftKey || theTarget!=resolveTarget(e))
		{ e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation(); }
	Popup.remove(); // close open popup (if any)
	return false;
}
//}}}
//{{{
// click in document background closes transient panels 
document.nestedSliders_savedOnClick=document.onclick;
document.onclick=function(ev) {
	if (!ev) var ev=window.event; var target=resolveTarget(ev);

	if (document.nestedSliders_savedOnClick)
		var retval=document.nestedSliders_savedOnClick.apply(this,arguments);
	// if click was inside a popup... leave transient panels alone
	var p=target; while (p) if (hasClass(p,"popup")) break; else p=p.parentNode;
	if (p) return retval;
	// if click was inside transient panel (or something contained by a transient panel), leave it alone
	var p=target; while (p) {
		if ((hasClass(p,"floatingPanel")||hasClass(p,"sliderPanel"))&&p.getAttribute("transient")=="true") break;
		p=p.parentNode;
	}
	if (p) return retval;
	// otherwise, find and close all transient panels...
	var all=document.all?document.all:document.getElementsByTagName("DIV");
	for (var i=0; i<all.length; i++) {
		 // if it is not a transient panel, or the click was on the button that opened this panel, don't close it.
		if (all[i].getAttribute("transient")!="true" || all[i].button==target) continue;
		// otherwise, if the panel is currently visible, close it by clicking it's button
		if (all[i].style.display!="none") window.onClickNestedSlider({target:all[i].button})
		if (!hasClass(all[i],"floatingPanel")&&!hasClass(all[i],"sliderPanel")) all[i].style.display="none";
	}
	return retval;
};
//}}}
//{{{
// adjust floating panel position based on button position
if (window.adjustSliderPos==undefined) window.adjustSliderPos=function(place,btn,panel) {
	if (hasClass(panel,"floatingPanel") && !hasClass(panel,"undocked")) {
		// see [[MoveablePanelPlugin]] for use of 'undocked'
		var rightEdge=document.body.offsetWidth-1;
		var panelWidth=panel.offsetWidth;
		var left=0;
		var top=btn.offsetHeight; 
		if (place.style.position=="relative" && findPosX(btn)+panelWidth>rightEdge) {
			left-=findPosX(btn)+panelWidth-rightEdge; // shift panel relative to button
			if (findPosX(btn)+left<0) left=-findPosX(btn); // stay within left edge
		}
		if (place.style.position!="relative") {
			var left=findPosX(btn);
			var top=findPosY(btn)+btn.offsetHeight;
			var p=place; while (p && !hasClass(p,'floatingPanel')) p=p.parentNode;
			if (p) { left-=findPosX(p); top-=findPosY(p); }
			if (left+panelWidth>rightEdge) left=rightEdge-panelWidth;
			if (left<0) left=0;
		}
		panel.style.left=left+"px"; panel.style.top=top+"px";
	}
}
//}}}
//{{{
// TW2.2+
// hijack Morpher stop handler so sliderPanel/floatingPanel overflow is visible after animation has completed
if (version.major+.1*version.minor+.01*version.revision>=2.2) {
	Morpher.prototype.coreStop = Morpher.prototype.stop;
	Morpher.prototype.stop = function() {
		this.coreStop.apply(this,arguments);
		var e=this.element;
		if (hasClass(e,"sliderPanel")||hasClass(e,"floatingPanel")) {
			// adjust panel overflow and position after animation
			e.style.overflow = "visible";
			if (window.adjustSliderPos) window.adjustSliderPos(e.parentNode,e.button,e);
		}
	};
}
//}}}
/***
|Description|Enables Ctrl + S hotkey to save changes (and not browser default action)|
|Version    |1.1|
|Author     |Yakov Litvin|
|Source     |https://github.com/YakovL/TiddlyWiki_QuickSavePlugin/blob/master/QuickSavePlugin.js|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
***/
//{{{
jQuery(document).on('keydown', null, function($e) {
    if(readOnly) return

    if($e.ctrlKey && $e.originalEvent.code === 'KeyS') {
        saveChanges()
    
        if($e.preventDefault) $e.preventDefault()
        return false
    }
})    
//}}}
<<defineRecipe [[ResponsiveThemePlugin]]
  parts:[[ResponsiveThemeSkeleton]] [[ResponsivePageTemplate]] [[ResponsiveStyleSheet]] [[TopLineMenu]]
  recipe:'
	const pt = parts["ResponsivePageTemplate"][0]
	parts["ResponsivePageTemplate"][0] = ""

	const ptStartMarker = "!Page Template\n"
	const skeleton = parts["ResponsiveThemeSkeleton"][0]
	const ptStartIndex = skeleton.indexOf(ptStartMarker) + ptStartMarker.length
	
	parts["ResponsiveThemeSkeleton"][0] = skeleton.substring(0, ptStartIndex) + pt + "\n"
		+ skeleton.substring(ptStartIndex)
	parts["ResponsiveStyleSheet"][0] = wrapAsCssAdder(parts["ResponsiveStyleSheet"][0], tid.title, "ResponsiveStyleSheet", { dontApply: true })
	// a hacky use of wrapAsCssAdder, should be a separate helper instead
	parts["TopLineMenu"][0] = wrapAsCssAdder(parts["TopLineMenu"][0], tid.title, "TopLineMenu", { dontApply: true })
  '
  plugin:true
  autoupdate:true
  reinstall:true
>>
<!--{{{-->
<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>
<!--}}}-->
/*{{{*/
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]]
/*}}}*/
/***
|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;
})();
//}}}
/***
|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
!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()
})
//}}}
/***
|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_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
<<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
		config.macros.saveChanges.SCM_orig_handler = config.macros.saveChanges.handler
		config.macros.saveChanges.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)
	}
}
//}}}
a temporal dev and demo polygon; may get to the core at some point
Responsive ~TiddlyWiki
txtUserName: Yakov Litvin
chkSaveBackups: false
chkAutoSave: true
chkInsertTabs: true
/***
|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','responsive');

//
// 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>>");
}

//}}}
{{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>
}}}
<<search>> <<darkMode label:"☀️/🌘">>
|Source |https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt|
|Version|0.2.5|
//{{{
[
  {
    "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_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": "Makes 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": "Fixes core upgrading for (at least) Timimi or MTS 1.7.0 and above, adds optional upgrade autocheck on start and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js",
    "description": "Notifies about extensions updates, introduces a macro and a backstage button to explore, install and update extensions"
  },
  {
    "url": "https://yakovl.github.io/TiddlyWiki_SharedTiddlersPlugin/#SharedTiddlersPlugin",
    "description": "Allows to use tiddlers from other TiddlyWikis (with or without importing them)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ImageGalleries/blob/master/FancyBox2Plugin.js",
    "description": "Adds a macro to create image galleries"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_MarkdeepPlugin/blob/master/MarkdeepPlugin.js",
    "description": "Create diagrams using the Markdeep syntax"
  }
]
//}}}