From b3652e48afaaa94cc9b38b329388ed56df984248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 2 Jun 2026 07:32:20 +0200 Subject: [PATCH 01/91] Remove debugging --- static/js/sync.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/js/sync.mjs b/static/js/sync.mjs index e432f15..291e0b9 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -9,9 +9,6 @@ export class Sync { }//}}} async run() {//{{{ - // XXX - Delete me - return - try { let duration = 0 // in ms From 2f27aeffb362ba74cd7030621d3aa3cc398df88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 2 Jun 2026 17:11:36 +0200 Subject: [PATCH 02/91] Fixed add_nodes function in database when adding a new top-level node --- sql/00002.sql | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 sql/00002.sql diff --git a/sql/00002.sql b/sql/00002.sql new file mode 100644 index 0000000..1775dc3 --- /dev/null +++ b/sql/00002.sql @@ -0,0 +1,168 @@ +CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid character varying, IN p_nodes jsonb) + LANGUAGE plpgsql +AS $procedure$ + +DECLARE + node_data jsonb; + node_updated timestamptz; + db_updated timestamptz; + db_uuid bpchar; + db_client bpchar; + db_client_seq int; + node_uuid bpchar; + parent_uuid bpchar; + +BEGIN + RAISE NOTICE '--------------------------'; + FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes) + LOOP + node_uuid = (node_data->>'UUID')::bpchar; + node_updated = (node_data->>'Updated')::timestamptz; + + IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN + parent_uuid = NULL; + ELSE + parent_uuid = node_data->>'ParentUUID'; + END IF; + + /* Retrieve the current modified timestamp for this node from the database. */ + SELECT + uuid, updated, client, client_sequence + INTO + db_uuid, db_updated, db_client, db_client_seq + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid; + + /* Is the node not in database? It needs to be created. */ + IF db_uuid IS NULL THEN + RAISE NOTICE '01 New node %', node_uuid; + INSERT INTO public."node" ( + user_id, "uuid", parent_uuid, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + parent_uuid, + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ); + CONTINUE; + END IF; + + + /* The client could send a specific node again if it didn't receive the OK from this procedure before. */ + IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN + RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq; + CONTINUE; + END IF; + + /* Determine if the incoming node data is to go into history or replace the current node. */ + IF db_updated > node_updated THEN + RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid; + /* Incoming node is going straight to history since it is older than the current node. */ + INSERT INTO node_history( + user_id, "uuid", parents, created, updated, + "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + VALUES( + p_user_id, + node_uuid, + (jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors", + (node_data->>'Created')::timestamptz, + (node_data->>'Updated')::timestamptz, + (node_data->>'Name')::varchar, + (node_data->>'Content')::text, + (node_data->>'Markdown')::bool, + '', /* content_encrypted */ + p_client_uuid, + (node_data->>'ClientSequence')::int + ) + ON CONFLICT (client, client_sequence) + DO NOTHING; + ELSE + RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence'); + /* Incoming node is newer and will replace the current node. + * + * The current node is copied to the node_history table and then modified in place + * with the incoming data. */ + INSERT INTO node_history( + user_id, "uuid", parents, + created, updated, "name", "content", markdown, "content_encrypted", + client, client_sequence + ) + SELECT + user_id, + "uuid", + ( + WITH RECURSIVE nodes AS ( + SELECT + uuid, + COALESCE(node.parent_uuid, '') AS parent_uuid, + name, + 0 AS depth + FROM node + WHERE + uuid = node_uuid + + UNION + + SELECT + n.uuid, + COALESCE(n.parent_uuid, '') AS parent_uuid, + n.name, + nr.depth+1 AS depth + FROM node n + INNER JOIN nodes nr ON n.uuid = nr.parent_uuid + ) + SELECT ARRAY ( + SELECT name + FROM nodes + ORDER BY depth DESC + OFFSET 1 /* discard itself */ + ) + ), + created, + updated, + name, + content, + markdown, + content_encrypted, + client, + client_sequence + FROM public."node" + WHERE + user_id = p_user_id AND + uuid = node_uuid + ON CONFLICT (client, client_sequence) + DO NOTHING; + + /* Current node in database is updated with incoming data. */ + UPDATE public."node" + SET + updated = (node_data->>'Updated')::timestamptz, + updated_seq = nextval('node_updates'), + name = (node_data->>'Name')::varchar, + content = (node_data->>'Content')::text, + markdown = (node_data->>'Markdown')::bool, + client = p_client_uuid, + client_sequence = (node_data->>'ClientSequence')::int + WHERE + user_id = p_user_id AND + uuid = node_uuid; + END IF; + + END LOOP; +END +$procedure$ +; From cc69f7194e05674e495b53a70732eb24bc2cc440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 2 Jun 2026 18:50:29 +0200 Subject: [PATCH 03/91] Sorting of "folders" before leafs --- static/js/node_store.mjs | 20 +++++++++++++++++++- static/js/page_node.mjs | 18 +++++++++++++++--- static/js/tree.mjs | 12 ++++++++---- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index d29923f..c72445c 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -176,14 +176,32 @@ export class NodeStore { const nodeStore = trx.objectStore('nodes') const index = nodeStore.index('byParent') const req = index.getAll(storeParent) + + const hasChildrenPromises = [] req.onsuccess = (event) => { const nodes = [] for (const i in event.target.result) { const nodeData = event.target.result[i] const node = this.node(nodeData.UUID, nodeData, newLevel) + + // Look for the key of any children, a hopefully fast way + // to tell if any children exists at all and this node is a + // "folder". Needed quite early on for sorting. + const promise = new Promise((resolve, reject) => { + const countReq = index.getKey(nodeData.UUID) + countReq.onsuccess = event => { + node.setHasChildren(event.target.result !== undefined) + resolve() + } + }) + hasChildrenPromises.push(promise) nodes.push(node) } - resolve(nodes) + + Promise.all(hasChildrenPromises) + .then(() => { + resolve(nodes) + }) } req.onerror = (event) => reject(event.target.error) }) diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index d151f77..d3f3a6e 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -257,8 +257,15 @@ customElements.define('n2-nodeui', N2PageNodeUI) export class Node { static sort(a, b) {//{{{ - if (a.data.Name < b.data.Name) return -1 - if (a.data.Name > b.data.Name) return 0 + // Nodes with children ("folders") are sorted first. + if (a._has_children && !b._has_children) return -1 + if (!a._has_children && b._has_children) return 1 + + // Otherwise sort by lowercased name. + const an = a.data.Name.toLowerCase() + const bn = b.data.Name.toLowerCase() + if (an < bn) return -1 + if (an > bn) return 1 return 0 }//}}} static create(name, parentUUID) {// {{{ @@ -286,6 +293,7 @@ export class Node { this.ParentUUID = nodeData.ParentUUID this._children_fetched = false + this._has_children = null // this will be set by nodeStore.getTreeNodes this.Children = [] this.Ancestors = [] @@ -322,6 +330,7 @@ export class Node { this.Children.sort(Node.sort) const numChildren = this.Children.length + this.setHasChildren(numChildren > 0) for (let i = 0; i < numChildren; i++) { if (i > 0) this.Children[i]._sibling_before = this.Children[i - 1] @@ -336,8 +345,11 @@ export class Node { return this.Children }//}}} + setHasChildren(v) {// {{{ + this._has_children = v + }// }}} hasChildren() {//{{{ - return this.Children.length > 0 + return this._has_children }//}}} getSiblingBefore() {// {{{ return this._sibling_before diff --git a/static/js/tree.mjs b/static/js/tree.mjs index b672742..408e281 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -91,7 +91,11 @@ export class N2Tree extends CustomHTMLElement { } _mbus.dispatch('TREE_TRUNK_FETCHED') }) - .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) + .catch(e => { + console.error(e) + console.log(e.type, e.error) + alert(e.error) + }) }//}}} getNodeExpanded(UUID) {//{{{ if (this.expandedNodes[UUID] === undefined) @@ -396,7 +400,7 @@ export class N2TreeNode extends CustomHTMLElement { this.render(true) }) - _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state => { + _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => { this.render(true) }) @@ -412,7 +416,7 @@ export class N2TreeNode extends CustomHTMLElement { return this // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. - const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID) + const expanded = this.node.hasChildren() && this.tree.getNodeExpanded(this.node.UUID) if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) { this.node.fetchChildren().then(() => this.children_populated = true) @@ -435,7 +439,7 @@ export class N2TreeNode extends CustomHTMLElement { } // The expand icon is only changed to not get a flickering when re-rendering. - if (this.node.Children.length === 0) + if (!this.node.hasChildren()) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`) else if (this.tree.getNodeExpanded(this.node.UUID)) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`) From 3c6648f22772141cd5dd32761f3f65875308ed00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Tue, 2 Jun 2026 21:05:28 +0200 Subject: [PATCH 04/91] Colorization of icons --- static/css/notes2.css | 12 +++++++- static/images/icon_history.svg | 49 +++++++++++++++++++++++++++++++++ static/images/icon_markdown.svg | 14 +++++----- static/images/icon_refresh.svg | 6 ++-- static/images/icon_save.svg | 16 +++++------ static/js/page_node.mjs | 11 ++++++-- static/js/tree.mjs | 8 +++--- 7 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 static/images/icon_history.svg diff --git a/static/css/notes2.css b/static/css/notes2.css index e02b90e..206c04b 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -4,12 +4,22 @@ --content-width: 900px; --thumbnail-width: 300px; --thumbnail-height: 100px; + + /* + --colorize: invert(10%) sepia(61%) saturate(5017%) hue-rotate(323deg) brightness(90%) contrast(109%); + */ + --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); + } html { background-color: #fff; } +.colorize { + filter: var(--colorize); +} + #notes2 { min-height: 100vh; @@ -21,7 +31,7 @@ html { "tree hum content content ding" "tree hum blank blank ding" ; - grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, calc(900px - 120px)) 120px minmax(16px, 1fr); + grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, calc(900px - 156px)) 156px minmax(16px, 1fr); grid-template-rows: min-content min-content 48px 1fr; diff --git a/static/images/icon_history.svg b/static/images/icon_history.svg new file mode 100644 index 0000000..af13eb9 --- /dev/null +++ b/static/images/icon_history.svg @@ -0,0 +1,49 @@ + + + + + + + + history + + + diff --git a/static/images/icon_markdown.svg b/static/images/icon_markdown.svg index f8d0aae..e926ea4 100644 --- a/static/images/icon_markdown.svg +++ b/static/images/icon_markdown.svg @@ -9,8 +9,8 @@ role="img" version="1.1" id="svg1" - sodipodi:docname="markdown.svg" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + sodipodi:docname="icon_markdown.svg" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -33,10 +33,10 @@ inkscape:zoom="0.70710678" inkscape:cx="453.96255" inkscape:cy="60.811183" - inkscape:window-width="2190" - inkscape:window-height="1401" - inkscape:window-x="1463" - inkscape:window-y="0" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + style="stroke-width:15.2119;fill:#000000;fill-opacity:1" /> <metadata id="metadata1"> <rdf:RDF> diff --git a/static/images/icon_refresh.svg b/static/images/icon_refresh.svg index a6aa907..870ff06 100644 --- a/static/images/icon_refresh.svg +++ b/static/images/icon_refresh.svg @@ -25,11 +25,11 @@ inkscape:document-units="mm" inkscape:zoom="23.548693" inkscape:cx="6.9218279" - inkscape:cy="12.5697" + inkscape:cy="12.612165" inkscape:window-width="1916" inkscape:window-height="1161" inkscape:window-x="0" - inkscape:window-y="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" showgrid="false" /> @@ -45,6 +45,6 @@ <path d="m 104.775,150.01875 a 1.5875,1.5875 0 0 1 -1.5875,-1.5875 c 0,-0.26458 0.0661,-0.52123 0.18521,-0.74083 l -0.38629,-0.3863 c -0.20638,0.32544 -0.32809,0.71173 -0.32809,1.12713 a 2.1166667,2.1166667 0 0 0 2.11667,2.11667 v 0.79375 l 1.05833,-1.05834 -1.05833,-1.05833 m 0,-2.91042 v -0.79375 l -1.05833,1.05834 1.05833,1.05833 v -0.79375 a 1.5875,1.5875 0 0 1 1.5875,1.5875 c 0,0.26458 -0.0661,0.52123 -0.18521,0.74083 l 0.38629,0.38629 c 0.20638,-0.32543 0.32809,-0.71172 0.32809,-1.12712 a 2.1166667,2.1166667 0 0 0 -2.11667,-2.11667 z" id="path1" - style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" /> + style="stroke-width:0.264583;fill:#000000;fill-opacity:1" /> </g> </svg> diff --git a/static/images/icon_save.svg b/static/images/icon_save.svg index 0846a73..6aa44b7 100644 --- a/static/images/icon_save.svg +++ b/static/images/icon_save.svg @@ -7,7 +7,7 @@ viewBox="0 0 4.7624998 4.7624998" version="1.1" id="svg1" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" sodipodi:docname="icon_save.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -24,12 +24,12 @@ inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="5.6568542" - inkscape:cx="41.454135" - inkscape:cy="-3.8890873" - inkscape:window-width="1093" - inkscape:window-height="1401" - inkscape:window-x="2560" - inkscape:window-y="0" + inkscape:cx="41.365747" + inkscape:cy="-3.7123106" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" /> <defs @@ -44,6 +44,6 @@ <path d="m 43.65625,209.81458 h -2.645833 v -1.05833 h 2.645833 m -0.79375,3.70417 a 0.79375,0.79375 0 0 1 -0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,0.79375 0.79375,0.79375 0 0 1 -0.79375,0.79375 m 1.322917,-4.23334 h -3.175 c -0.293688,0 -0.529167,0.23813 -0.529167,0.52917 v 3.70417 a 0.52916667,0.52916667 0 0 0 0.529167,0.52916 h 3.704166 a 0.52916667,0.52916667 0 0 0 0.529167,-0.52916 v -3.175 z" id="path1" - style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" /> + style="stroke-width:0.264583;fill:#000000;fill-opacity:1" /> </g> </svg> diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index d3f3a6e..fecfa0c 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -9,7 +9,7 @@ export class N2PageNodeUI extends CustomHTMLElement { <style> .el-functions { display: grid; - grid-template-columns: 1fr min-content min-content; + grid-template-columns: 1fr repeat(3, min-content); grid-gap: 8px; align-items: center; justify-items: end; @@ -25,9 +25,10 @@ export class N2PageNodeUI extends CustomHTMLElement { <div data-el="node-markdown" tabindex=1></div> <div data-el="functions"> - <img data-el="icon-markdown"> <img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg"> - <img data-el="icon-table-format" src="/images/${_VERSION}/icon_table.svg"> + <img data-el="icon-markdown"> + <img data-el="icon-table-format" class="colorize" src="/images/${_VERSION}/icon_table.svg"> + <img data-el="icon-history" class="colorize" src="/images/${_VERSION}/icon_history.svg"> </div> ` }// }}} @@ -49,12 +50,14 @@ export class N2PageNodeUI extends CustomHTMLElement { _mbus.subscribe('NODE_MODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save.svg` + this.elIconSave.classList.add('colorize') this.renderName() }) _mbus.subscribe('NODE_UNMODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg` + this.elIconSave.classList.remove('colorize') }) _mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown())) @@ -123,10 +126,12 @@ export class N2PageNodeUI extends CustomHTMLElement { case true: this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg` + this.elIconMarkdown.classList.add('colorize') this.classList.add('show-markdown') break case false: this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg` + this.elIconMarkdown.classList.remove('colorize') this.classList.remove('show-markdown') break case null: diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 408e281..3bbf2df 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -12,8 +12,8 @@ export class N2Tree extends CustomHTMLElement { <img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> </div> <div class="icons"> - <img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" /> - <img data-el="settings" class='settings' src="/images/${_VERSION}/icon_settings.svg" /> + <img data-el="sync" class='sync colorize' src="/images/${_VERSION}/icon_refresh.svg" /> + <img data-el="settings" class='settings colorize' src="/images/${_VERSION}/icon_settings.svg" /> </div> <div data-el="treenodes"></div> ` @@ -50,10 +50,10 @@ export class N2Tree extends CustomHTMLElement { this.populateFirstLevel() /* XXX - set color */ - let color = new Color(255, 96, 80) + let color = new Color(0x80, 0x00, 0x33) let solver = new Solver(color) let result = solver.solve() - this.elSettings.style.filter = result.filter + // console.log(result.filter) }// }}} render() {// {{{ if (this.rendered) From 1d75aa8c3e1c8896bdddbb2ff15637b3a9f69f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Tue, 2 Jun 2026 21:16:52 +0200 Subject: [PATCH 05/91] Added history page --- static/css/notes2.css | 9 +++++++++ static/js/page_node.mjs | 1 + views/pages/notes2.gotmpl | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/static/css/notes2.css b/static/css/notes2.css index 206c04b..01d6143 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -176,6 +176,15 @@ html { } } } + + &.history { + #page-history { + display: contents; + n2-pagehistory { + grid-area: content; + } + } + } } #crumbs { diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index fecfa0c..341f155 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -92,6 +92,7 @@ export class N2PageNodeUI extends CustomHTMLElement { this.node.setContent(this.elNodeContent.value) }) + this.elIconHistory.addEventListener('click', ()=>_mbus.dispatch('SHOW_PAGE', { page: 'history' })) this.showMarkdown(true) }// }}} diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 80c084a..203e0a4 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -14,6 +14,11 @@ <n2-syncprogress></n2-syncprogress> <n2-nodeui id="note"></n2-nodeui> </div> + + <!-- History --> + <div id="page-history"> + <n2-pagehistory></n2-pagehistory> + </div> </div> </div> @@ -28,6 +33,7 @@ import {API} from 'api' import {Sync} from 'sync' import { } from '/js/{{ .VERSION }}/page_storage.mjs' + import { } from '/js/{{ .VERSION }}/page_history.mjs' import { } from '/js/{{ .VERSION }}/file.mjs' window.Sync = Sync From 3bd0819cbce41178f9c0d59a46044061ebe67b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Tue, 2 Jun 2026 22:50:15 +0200 Subject: [PATCH 06/91] Better mobile view --- static/css/notes2.css | 117 +++++++++++++++++++++++++++++++------ static/js/page_history.mjs | 15 +++++ static/js/tree.mjs | 21 ++++++- views/pages/notes2.gotmpl | 13 ++++- 4 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 static/js/page_history.mjs diff --git a/static/css/notes2.css b/static/css/notes2.css index 01d6143..ad07770 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -10,6 +10,8 @@ */ --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); + + --show-tree: 0px; } html { @@ -25,26 +27,93 @@ html { display: grid; grid-template-areas: - "tree hum crumbs crumbs ding" - "tree hum name name ding" - "tree hum sync functions ding" - "tree hum content content ding" - "tree hum blank blank ding" + "show-tree tree hum crumbs crumbs ding" + "show-tree tree hum name name ding" + "show-tree tree hum sync functions ding" + "show-tree tree hum content content ding" + "show-tree tree hum blank blank ding" ; - grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, calc(900px - 156px)) 156px minmax(16px, 1fr); + + grid-template-columns: var(--show-tree) min-content minmax(32px, 1fr) minmax(min-content, calc(900px - 156px)) 156px minmax(32px, 1fr); grid-template-rows: min-content min-content 48px 1fr; + &.hide-tree { + --show-tree: 32px; + #tree { + border-right: none; + } + n2-tree { + display: none; - @media only screen and (max-width: 600px) { + } + } + + #show-tree { + grid-area: show-tree; + color: #333; + font-weight: bold; + border-right: 2px solid #ddd; + + display: grid; + justify-items: center; + align-items: start; + + padding-top: 8px; + font-size: 1.25em; + + div div { + display: inline-block; + writing-mode: vertical-rl; + transform: rotate(180deg); + } + } + + n2-nodeui { + .el-functions { + width: calc(100% - 32px); + } + + .el-node-markdown { + overflow-wrap: anywhere; + width: calc(100% - 32px); + } + } + + + +} + +@media only screen and (max-width: 800px) { + #notes2 { grid-template-areas: - "crumbs" - "sync" - "name" - "content" - "blank" + "show-tree hum crumbs ding" + "show-tree hum name ding" + "show-tree hum functions ding" + "show-tree hum content ding" + "show-tree hum blank ding" + "show-tree hum sync ding" ; - grid-template-columns: 1fr; + grid-template-columns: 32px 16px 1fr 16px; + + &.show-tree { + + grid-template-areas: + "tree" + ; + grid-template-columns: 100%; + grid-template-rows: + 1fr; + + #tree { + display: grid; + width: 100%; + } + + #main-page, #show-tree { + display: none; + } + } #tree { display: none; @@ -55,8 +124,18 @@ html { top: 4px; } } - } + n2-nodeui { + .el-functions { + width: calc(100% - 32px); + } + + .el-node-markdown { + overflow-wrap: anywhere; + width: calc(100% - 32px); + } + } + } } #tree { @@ -101,8 +180,7 @@ html { } &:focus-within { - n2-tree { - } + n2-tree {} } @@ -171,6 +249,7 @@ html { &.storage { #page-storage { display: contents; + n2-pagestorage { grid-area: content; } @@ -180,6 +259,7 @@ html { &.history { #page-history { display: contents; + n2-pagehistory { grid-area: content; } @@ -196,11 +276,10 @@ html { margin: 0 16px 16px 16px; n2-crumbs { - background: #e4e4e4; + background: #eaeaea; display: flex; flex-wrap: wrap; padding: 8px 16px; - background: #e4e4e4; color: #333; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; @@ -317,7 +396,7 @@ n2-nodeui { color: #333; font-weight: bold; text-align: center; - font-size: 1.15em; + font-size: 1.5em; margin-top: 8px; margin-bottom: 0px; } diff --git a/static/js/page_history.mjs b/static/js/page_history.mjs new file mode 100644 index 0000000..a931faa --- /dev/null +++ b/static/js/page_history.mjs @@ -0,0 +1,15 @@ +import { CustomHTMLElement } from './lib/custom_html_element.mjs' + +export class N2PageHistory extends CustomHTMLElement { + static { + this.tmpl = document.createElement('template') + this.tmpl.innerHTML = ` + <div>History</div> + ` + } + + constructor() { + super() + } +} +customElements.define('n2-pagehistory', N2PageHistory) diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 3bbf2df..55a37ee 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -6,13 +6,23 @@ export class N2Tree extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` - <div data-el="logo" id="logo"> + <style> + n2-tree { + .el-hide-tree { + font-size: 1.25em; + font-weight: bold; + cursor: pointer; + } + } + </style> + <div id="logo"> <img src="/images/${_VERSION}/logo_small.svg" /> - <img src="/images/${_VERSION}/logo.svg" /> - <img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> + <img data-el="logo" src="/images/${_VERSION}/logo.svg" /> + <div data-el="hide-tree"><</div> </div> <div class="icons"> <img data-el="sync" class='sync colorize' src="/images/${_VERSION}/icon_refresh.svg" /> + <img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> <img data-el="settings" class='settings colorize' src="/images/${_VERSION}/icon_settings.svg" /> </div> <div data-el="treenodes"></div> @@ -35,6 +45,11 @@ export class N2Tree extends CustomHTMLElement { this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search')) this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) + this.elHideTree.addEventListener('click', event=>{ + event.stopPropagation() + document.getElementById('notes2').classList.add('hide-tree') + document.getElementById('notes2').classList.remove('show-tree') + }) _mbus.subscribe('NODE_MODIFIED', ({ detail }) => { const node = detail.data.node diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 203e0a4..fe74240 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,5 +1,14 @@ {{ define "page" }} +<script> + function showTree() { + const notes2 = document.getElementById('notes2') + notes2.classList.remove('hide-tree') + notes2.classList.add('show-tree') + } +</script> + <div id="notes2"> + <div id="show-tree" onclick="showTree()">></div> <div id="tree" tabindex=0></div> <div id="main-page"> @@ -11,7 +20,7 @@ <!-- Node editing --> <div id="page-node"> <div id="crumbs"></div> - <n2-syncprogress></n2-syncprogress> + <!--n2-syncprogress></n2-syncprogress--> <n2-nodeui id="note"></n2-nodeui> </div> @@ -25,8 +34,6 @@ <link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css"> <script type="module"> - - import {NodeStore} from '/js/{{ .VERSION }}/node_store.mjs' import {App} from "/js/{{ .VERSION }}/app.mjs" From 6bb277379527bc94e2ef841fb3c204ce0ec18a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Tue, 2 Jun 2026 22:50:33 +0200 Subject: [PATCH 07/91] Bumped to v7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 96e26ca..8da11f1 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v6" +const VERSION = "v7" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 From bea865dd82e148738b1f66fd5b9f276575ac7eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Tue, 2 Jun 2026 23:01:51 +0200 Subject: [PATCH 08/91] Added missing static files to service worker, bumped to v8 --- main.go | 2 +- static/service_worker.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 8da11f1..d927b23 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v7" +const VERSION = "v8" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 diff --git a/static/service_worker.js b/static/service_worker.js index 80b79a1..b01d9cc 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -22,17 +22,20 @@ const CACHED_ASSETS = [ '/js/{{ .VERSION }}/app.mjs', '/js/{{ .VERSION }}/checklist.mjs', '/js/{{ .VERSION }}/crypto.mjs', + '/js/{{ .VERSION }}/file.mjs', '/js/{{ .VERSION }}/key.mjs', + '/js/{{ .VERSION }}/lib/css_colorize.js', '/js/{{ .VERSION }}/lib/custom_html_element.mjs', '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', '/js/{{ .VERSION }}/lib/sjcl.js', '/js/{{ .VERSION }}/marked_position.mjs', '/js/{{ .VERSION }}/mbus.mjs', - '/js/{{ .VERSION }}/page_node.mjs', - '/js/{{ .VERSION }}/page_storage.mjs', '/js/{{ .VERSION }}/node_store.mjs', '/js/{{ .VERSION }}/notes2.mjs', + '/js/{{ .VERSION }}/page_history.mjs', + '/js/{{ .VERSION }}/page_node.mjs', + '/js/{{ .VERSION }}/page_storage.mjs', '/js/{{ .VERSION }}/sync.mjs', '/js/{{ .VERSION }}/tree.mjs', ] From a3864d2b556ffc96ed4221e8e7265308323f5486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Wed, 3 Jun 2026 20:31:23 +0200 Subject: [PATCH 09/91] Tree expansion handled better --- static/css/notes2.css | 146 +++++++++++++++++----------------- static/images/icon_search.svg | 4 +- static/js/app.mjs | 1 + static/js/tree.mjs | 65 ++++++++++++++- static/service_worker.js | 7 +- views/pages/notes2.gotmpl | 10 +-- 6 files changed, 143 insertions(+), 90 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index ad07770..14f475e 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -10,8 +10,7 @@ */ --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); - - --show-tree: 0px; + --tree-expander: 0px; } html { @@ -22,95 +21,86 @@ html { filter: var(--colorize); } +/* ------------------------------------- * +* Default application grid in wide mode * +* ------------------------------------- */ #notes2 { min-height: 100vh; display: grid; grid-template-areas: - "show-tree tree hum crumbs crumbs ding" - "show-tree tree hum name name ding" - "show-tree tree hum sync functions ding" - "show-tree tree hum content content ding" - "show-tree tree hum blank blank ding" + "tree-expander tree pad1 crumbs crumbs pad2" + "tree-expander tree pad1 name name pad2" + "tree-expander tree pad1 sync functions pad2" + "tree-expander tree pad1 content content pad2" ; - grid-template-columns: var(--show-tree) min-content minmax(32px, 1fr) minmax(min-content, calc(900px - 156px)) 156px minmax(32px, 1fr); + grid-template-columns: + /* Tree-expander */ + var(--tree-expander) + /* Tree */ + min-content minmax(32px, 1fr) + /* Sync */ + minmax(min-content, calc(900px - 156px)) + /* Functions */ + 156px + /* Content */ + minmax(32px, 1fr); + grid-template-rows: - min-content min-content 48px 1fr; + /* Crumbs */ + min-content + /* Name */ + min-content + /* Sync */ + 48px + /* Content */ + 1fr; + + /* Tree expander is collapsed as default */ + --tree-expander: 0px; &.hide-tree { - --show-tree: 32px; + --tree-expander: 32px; + #tree { border-right: none; } + n2-tree { display: none; } } - - #show-tree { - grid-area: show-tree; - color: #333; - font-weight: bold; - border-right: 2px solid #ddd; - - display: grid; - justify-items: center; - align-items: start; - - padding-top: 8px; - font-size: 1.25em; - - div div { - display: inline-block; - writing-mode: vertical-rl; - transform: rotate(180deg); - } - } - - n2-nodeui { - .el-functions { - width: calc(100% - 32px); - } - - .el-node-markdown { - overflow-wrap: anywhere; - width: calc(100% - 32px); - } - } - - - } +/* ------------------------------- * + * Application grid in narrow mode * + * ------------------------------- */ @media only screen and (max-width: 800px) { #notes2 { grid-template-areas: - "show-tree hum crumbs ding" - "show-tree hum name ding" - "show-tree hum functions ding" - "show-tree hum content ding" - "show-tree hum blank ding" - "show-tree hum sync ding" + "tree-expander pad1 crumbs pad2" + "tree-expander pad1 name pad2" + "tree-expander pad1 functions pad2" + "tree-expander pad1 content pad2" + "tree-expander pad1 blank pad2" + "tree-expander pad1 sync pad2" ; grid-template-columns: 32px 16px 1fr 16px; &.show-tree { - - grid-template-areas: - "tree" - ; + grid-template-areas: "tree"; grid-template-columns: 100%; - grid-template-rows: - 1fr; + grid-template-rows: 1fr; #tree { display: grid; width: 100%; } - - #main-page, #show-tree { + + #main-page, + #show-tree { display: none; } } @@ -118,23 +108,26 @@ html { #tree { display: none; } + } +} - n2-syncprogress { - .el-count { - top: 4px; - } - } +#tree-expander { + grid-area: tree-expander; + color: #333; + font-weight: bold; + border-right: 2px solid #ddd; - n2-nodeui { - .el-functions { - width: calc(100% - 32px); - } + display: grid; + justify-items: center; + align-items: start; - .el-node-markdown { - overflow-wrap: anywhere; - width: calc(100% - 32px); - } - } + padding-top: 8px; + font-size: 1.25em; + + div div { + display: inline-block; + writing-mode: vertical-rl; + transform: rotate(180deg); } } @@ -181,10 +174,8 @@ html { &:focus-within { n2-tree {} - } - .node { display: grid; grid-template-columns: 40px min-content; @@ -233,6 +224,9 @@ html { } } +n2-nodeui { +} + [id^="page-"] { display: none; } @@ -403,6 +397,7 @@ n2-nodeui { .el-functions { grid-area: functions; + width: calc(100% - 32px); } .el-node-content { @@ -438,6 +433,9 @@ n2-nodeui { border-top: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; margin-bottom: 32px; + + overflow-wrap: anywhere; + width: calc(100% - 32px); } &.show-markdown { diff --git a/static/images/icon_search.svg b/static/images/icon_search.svg index 6de83dd..7b05f5c 100644 --- a/static/images/icon_search.svg +++ b/static/images/icon_search.svg @@ -35,7 +35,7 @@ inkscape:window-width="1916" inkscape:window-height="1161" inkscape:window-x="0" - inkscape:window-y="18" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:showpageshadow="true" inkscape:pagecheckerboard="0" @@ -62,6 +62,6 @@ <path d="m 78.736803,96.575592 a 40.634474,40.634474 0 0 1 40.634477,40.634438 c 0,10.06486 -3.68838,19.31694 -9.75227,26.44372 l 1.68795,1.68701 h 4.93863 l 31.2573,31.25736 -9.37719,9.37731 -31.25729,-31.25737 v -4.93863 l -1.68795,-1.68701 c -7.126666,6.06378 -16.378815,9.75204 -26.443681,9.75204 A 40.634474,40.634474 0 0 1 38.102322,137.21003 40.634474,40.634474 0 0 1 78.736803,96.575592 m 0,12.502758 c -15.628636,0 -28.131559,12.50299 -28.131559,28.13168 0,15.62868 12.502923,28.13144 28.131559,28.13144 15.628635,0 28.131557,-12.50276 28.131557,-28.13144 0,-15.62869 -12.502922,-28.13168 -28.131557,-28.13168 z" id="path1" - style="stroke-width:6.25145;fill:#fe5f55;fill-opacity:1" /> + style="stroke-width:6.25145;fill:#000000;fill-opacity:1" /> </g> </svg> diff --git a/static/js/app.mjs b/static/js/app.mjs index feeec3a..8ff6229 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -206,6 +206,7 @@ export class App { _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render())) _mbus.dispatch('NODE_UI_OPEN', node) _mbus.dispatch('NODE_UNMODIFIED') + _mbus.dispatch('TREE_EXPANSION', { expand: false, when: 'narrow' }) // Scrolls node into view. this.tree.makeVisible(node) diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 55a37ee..04555f6 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -2,6 +2,62 @@ import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { Color, Solver } from './lib/css_colorize.mjs' +// TreeExpandedHandler is responsible for collapsing or expanding +// the node tree, wide view or narrow "mobile" view. +class TreeExpansionHandler {// {{{ + constructor() { + this.isNarrow = false + this.initializeMediaHandler() + this.initializeBusEvents() + } + + initializeBusEvents() { + _mbus.subscribe('TREE_EXPANSION', ({ detail }) => { + // When a node is selected on the screen and the screen + // is narrow the tree is automatically hidden. + // + // Can't always hide the tree automatically when a node + // is selected since the wide mode shows the tree as standard. + if (detail.data?.when == 'narrow' && !this.isNarrow) + return + + this.treeExpansion(detail.data?.expand) + }) + } + + initializeMediaHandler() { + const query = window.matchMedia('(max-width: 800px)') + query.addEventListener('change', event => this.screenNarrowHandler(event)) + + // Run once to set initial state, instead of needing to toggle state. + this.screenNarrowHandler(query) + } + + // When screen becomes narrow, the tree is automatically hidden. + // Primary purpose is to read content, not browse, which is why + // the tree is hidden as standard. + screenNarrowHandler(event) { + this.isNarrow = event.matches + + if (this.isNarrow) + this.treeExpansion(false) + else + this.treeExpansion(true) + } + + treeExpansion(expanded) { + const notes2 = document.getElementById('notes2') + + if (expanded) { + notes2.classList.remove('hide-tree') + notes2.classList.add('show-tree') + } else { + notes2.classList.add('hide-tree') + notes2.classList.remove('show-tree') + } + } +}// }}} + export class N2Tree extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') @@ -22,7 +78,7 @@ export class N2Tree extends CustomHTMLElement { </div> <div class="icons"> <img data-el="sync" class='sync colorize' src="/images/${_VERSION}/icon_refresh.svg" /> - <img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> + <img data-el="search" class='search colorize' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" /> <img data-el="settings" class='settings colorize' src="/images/${_VERSION}/icon_settings.svg" /> </div> <div data-el="treenodes"></div> @@ -41,14 +97,15 @@ export class N2Tree extends CustomHTMLElement { this.selectedNode = null this.rendered = false + new TreeExpansionHandler() + this.addEventListener('keydown', event => this.keyHandler(event)) this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search')) this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) - this.elHideTree.addEventListener('click', event=>{ + this.elHideTree.addEventListener('click', event => { event.stopPropagation() - document.getElementById('notes2').classList.add('hide-tree') - document.getElementById('notes2').classList.remove('show-tree') + _mbus.dispatch('TREE_EXPANSION', { expand: false }) }) _mbus.subscribe('NODE_MODIFIED', ({ detail }) => { diff --git a/static/service_worker.js b/static/service_worker.js index b01d9cc..55a5f09 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -10,12 +10,17 @@ const CACHED_ASSETS = [ '/images/{{ .VERSION }}/collapsed.svg', '/images/{{ .VERSION }}/expanded.svg', + '/images/{{ .VERSION }}/icon_history.svg', '/images/{{ .VERSION }}/icon_markdown_hollow.svg', '/images/{{ .VERSION }}/icon_markdown.svg', '/images/{{ .VERSION }}/icon_refresh.svg', '/images/{{ .VERSION }}/icon_save_disabled.svg', + '/images/{{ .VERSION }}/icon_save.svg', '/images/{{ .VERSION }}/icon_search.svg', + '/images/{{ .VERSION }}/icon_settings.svg', + '/images/{{ .VERSION }}/icon_table.svg', '/images/{{ .VERSION }}/leaf.svg', + '/images/{{ .VERSION }}/logo_small.svg', '/images/{{ .VERSION }}/logo.svg', '/js/{{ .VERSION }}/api.mjs', @@ -24,7 +29,7 @@ const CACHED_ASSETS = [ '/js/{{ .VERSION }}/crypto.mjs', '/js/{{ .VERSION }}/file.mjs', '/js/{{ .VERSION }}/key.mjs', - '/js/{{ .VERSION }}/lib/css_colorize.js', + '/js/{{ .VERSION }}/lib/css_colorize.mjs', '/js/{{ .VERSION }}/lib/custom_html_element.mjs', '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index fe74240..f15bbb5 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -1,14 +1,6 @@ {{ define "page" }} -<script> - function showTree() { - const notes2 = document.getElementById('notes2') - notes2.classList.remove('hide-tree') - notes2.classList.add('show-tree') - } -</script> - <div id="notes2"> - <div id="show-tree" onclick="showTree()">></div> + <div id="tree-expander" onclick="window._mbus.dispatch('TREE_EXPANSION', { expand: true })">></div> <div id="tree" tabindex=0></div> <div id="main-page"> From d100b64108d3b058c9809304b1f498c2b45f35aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Wed, 3 Jun 2026 20:31:39 +0200 Subject: [PATCH 10/91] Bumped to v9 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d927b23..156b916 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v8" +const VERSION = "v9" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 From d77b23b58825c494838563ffea2691be6a3013a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= <magnus.ahall@gibon.se> Date: Wed, 3 Jun 2026 22:10:37 +0200 Subject: [PATCH 11/91] Layout changes --- static/css/notes2.css | 92 ++++++++++++++++++------------------- static/images/icon_home.svg | 71 ++++++++++++++++++++++++++++ static/js/app.mjs | 14 +++++- static/js/page_node.mjs | 4 +- static/service_worker.js | 1 + 5 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 static/images/icon_home.svg diff --git a/static/css/notes2.css b/static/css/notes2.css index 14f475e..a2dbc11 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -11,6 +11,7 @@ --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); --tree-expander: 0px; + --functions-width: 150px; } html { @@ -30,8 +31,7 @@ html { display: grid; grid-template-areas: "tree-expander tree pad1 crumbs crumbs pad2" - "tree-expander tree pad1 name name pad2" - "tree-expander tree pad1 sync functions pad2" + "tree-expander tree pad1 name functions pad2" "tree-expander tree pad1 content content pad2" ; @@ -41,9 +41,9 @@ html { /* Tree */ min-content minmax(32px, 1fr) /* Sync */ - minmax(min-content, calc(900px - 156px)) + minmax(min-content, calc(var(--content-width) - var(--functions-width))) /* Functions */ - 156px + var(--functions-width) /* Content */ minmax(32px, 1fr); @@ -52,8 +52,6 @@ html { min-content /* Name */ min-content - /* Sync */ - 48px /* Content */ 1fr; @@ -80,14 +78,11 @@ html { @media only screen and (max-width: 800px) { #notes2 { grid-template-areas: - "tree-expander pad1 crumbs pad2" - "tree-expander pad1 name pad2" - "tree-expander pad1 functions pad2" - "tree-expander pad1 content pad2" - "tree-expander pad1 blank pad2" - "tree-expander pad1 sync pad2" + "tree-expander pad1 crumbs crumbs pad2" + "tree-expander pad1 name functions pad2" + "tree-expander pad1 content content pad2" ; - grid-template-columns: 32px 16px 1fr 16px; + grid-template-columns: 32px 16px 1fr var(--functions-width) 16px; &.show-tree { grid-template-areas: "tree"; @@ -114,6 +109,7 @@ html { #tree-expander { grid-area: tree-expander; color: #333; + background-color: #eee; font-weight: bold; border-right: 2px solid #ddd; @@ -224,9 +220,6 @@ html { } } -n2-nodeui { -} - [id^="page-"] { display: none; } @@ -265,28 +258,19 @@ n2-nodeui { grid-area: crumbs; display: grid; align-items: start; - justify-items: center; + justify-items: start; height: min-content; - margin: 0 16px 16px 16px; + margin: 0 16px 16px 0px; n2-crumbs { - background: #eaeaea; display: flex; flex-wrap: wrap; - padding: 8px 16px; + align-items: center; + padding: 16px 0px; color: #333; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; - &.node-modified { - background-color: var(--color1); - color: var(--color2); - - .crumb:after { - color: var(--color2); - } - } - n2-crumb { margin-right: 8px; cursor: pointer; @@ -299,21 +283,24 @@ n2-nodeui { } } - n2-crumb:after { + + n2-crumb:before { content: ">"; font-weight: bold; color: var(--color1) } - n2-crumb:last-child { - margin-right: 0; - } - n2-crumb:last-child:after { - content: ''; - margin-left: 0px; - } + n2-crumb.home { + &:before { + content: ''; + margin-left: 0px; + } + img { + height: 24px; + } + } } } @@ -385,19 +372,33 @@ n2-syncprogress { n2-nodeui { margin-bottom: 32px; + &.node-modified:before { + content: 'h'; + z-index: 8192; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + height: 4px; + + background-color: var(--color1); + color: var(--color2); + } + .el-name { grid-area: name; color: #333; font-weight: bold; - text-align: center; - font-size: 1.5em; + font-size: 1.75em; margin-top: 8px; margin-bottom: 0px; } .el-functions { grid-area: functions; - width: calc(100% - 32px); + justify-self: end; + align-self: end; + margin-bottom: 6px; } .el-node-content { @@ -405,6 +406,7 @@ n2-nodeui { justify-self: center; word-wrap: break-word; font-family: monospace; + font-size: 1em; color: #333; width: 100%; @@ -418,12 +420,9 @@ n2-nodeui { border-left: none; border-right: none; border-top: 1px solid #e0e0e0; - border-bottom: 1px solid #e0e0e0; + border-bottom: none; + margin-top: 8px; margin-bottom: 32px; - - &:invalid { - padding-top: 16px; - } } .el-node-markdown { @@ -431,11 +430,10 @@ n2-nodeui { display: none; border-top: 1px solid #e0e0e0; - border-bottom: 1px solid #e0e0e0; + margin-top: 8px; margin-bottom: 32px; overflow-wrap: anywhere; - width: calc(100% - 32px); } &.show-markdown { diff --git a/static/images/icon_home.svg b/static/images/icon_home.svg new file mode 100644 index 0000000..3bf96d5 --- /dev/null +++ b/static/images/icon_home.svg @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="24" + height="20.000013" + viewBox="0 0 6.3499998 5.2916702" + version="1.1" + id="svg1" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" + sodipodi:docname="icon_home.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="px" + inkscape:zoom="34.879392" + inkscape:cx="11.797797" + inkscape:cy="8.3717056" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer1" + showgrid="true"> + <inkscape:grid + id="grid1" + units="px" + originx="0" + originy="0" + spacingx="0.26458333" + spacingy="0.26458333" + empcolor="#0099e5" + empopacity="0.30196078" + color="#0099e5" + opacity="0.14901961" + empspacing="5" + enabled="true" + visible="true" /> + </sodipodi:namedview> + <defs + id="defs1" /> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-102.39375,-146.31458)"> + <title + id="title1">home-outline + + + + diff --git a/static/js/app.mjs b/static/js/app.mjs index 8ff6229..4978b98 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -237,7 +237,7 @@ class N2Crumbs extends CustomHTMLElement { ) ) - const start = new N2Crumb('Start', ROOT_NODE) + const start = new N2Crumb('', ROOT_NODE) crumbs.push(start) this.replaceChildren(...crumbs.reverse()) @@ -255,8 +255,18 @@ class N2Crumb extends CustomHTMLElement { }// }}} constructor(label, uuid) {// {{{ super() - this.classList.add('crumb') + // The house makes it a bit more graphical than just a bunch of text. + if (uuid === ROOT_NODE) { + const start = document.createElement('div') + start.innerHTML = `` + start.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: ROOT_NODE, dontPush: false, dontExpand: true })) + this.classList.add('home') + this.replaceChildren(start) + return + } + + this.classList.add('crumb') this.label = label this.uuid = uuid diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs index 341f155..81d7c33 100644 --- a/static/js/page_node.mjs +++ b/static/js/page_node.mjs @@ -48,14 +48,14 @@ export class N2PageNodeUI extends CustomHTMLElement { }) _mbus.subscribe('NODE_MODIFIED', () => { - document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') + this.classList.add('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save.svg` this.elIconSave.classList.add('colorize') this.renderName() }) _mbus.subscribe('NODE_UNMODIFIED', () => { - document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified') + this.classList.remove('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg` this.elIconSave.classList.remove('colorize') }) diff --git a/static/service_worker.js b/static/service_worker.js index 55a5f09..b6a1a13 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -11,6 +11,7 @@ const CACHED_ASSETS = [ '/images/{{ .VERSION }}/collapsed.svg', '/images/{{ .VERSION }}/expanded.svg', '/images/{{ .VERSION }}/icon_history.svg', + '/images/{{ .VERSION }}/icon_home.svg', '/images/{{ .VERSION }}/icon_markdown_hollow.svg', '/images/{{ .VERSION }}/icon_markdown.svg', '/images/{{ .VERSION }}/icon_refresh.svg', From ee327a6337d607ef8f689fa7cd2080b13f77f52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 3 Jun 2026 22:10:51 +0200 Subject: [PATCH 12/91] Bumped to v10 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 156b916..169c3de 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v9" +const VERSION = "v10" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 From 131c6133ba7901672eaae2036d44f6a53a681dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 3 Jun 2026 22:22:29 +0200 Subject: [PATCH 13/91] Fixed path in logo small --- static/images/logo_small.svg | 30 ++++++++++++++++++------------ static/js/tree.mjs | 1 + 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/static/images/logo_small.svg b/static/images/logo_small.svg index cb83d39..47eabde 100644 --- a/static/images/logo_small.svg +++ b/static/images/logo_small.svg @@ -7,7 +7,7 @@ viewBox="0 0 7.5652731 5.2916666" version="1.1" id="svg1" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" sodipodi:docname="logo_small.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="16.477217" - inkscape:cx="48.460855" - inkscape:cy="5.2193281" - inkscape:window-width="2190" - inkscape:window-height="1401" - inkscape:window-x="1463" - inkscape:window-y="18" + inkscape:zoom="8.2386085" + inkscape:cx="48.491198" + inkscape:cy="5.219328" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1" /> + N2 + y="213.81186">N2 diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 04555f6..fc861e4 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -68,6 +68,7 @@ export class N2Tree extends CustomHTMLElement { font-size: 1.25em; font-weight: bold; cursor: pointer; + margin-left: 16px; } } From 44ee1ac94b3c01e0eae0995d5383c576b1651fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 4 Jun 2026 07:47:27 +0200 Subject: [PATCH 14/91] Design changes --- static/css/notes2.css | 36 ++++----------------------------- static/js/tree.mjs | 47 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index a2dbc11..5a3e0dd 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -10,6 +10,7 @@ */ --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); + --line-color: #ccc; --tree-expander: 0px; --functions-width: 150px; } @@ -111,7 +112,7 @@ html { color: #333; background-color: #eee; font-weight: bold; - border-right: 2px solid #ddd; + border-right: 1px solid var(--line-color); display: grid; justify-items: center; @@ -127,6 +128,7 @@ html { } } + #tree { grid-area: tree; display: grid; @@ -134,33 +136,7 @@ html { color: #444; z-index: 100; - border-right: 2px solid #ddd; - - #logo { - display: grid; - grid-template-columns: min-content 1fr min-content; - align-items: center; - justify-items: start; - cursor: pointer; - padding: 16px; - border-bottom: 1px solid #ccc; - - .el-search { - justify-self: end; - } - - img:first-child { - height: 24px; - margin-right: 8px; - } - } - - .icons { - display: flex; - justify-content: center; - margin: 16px 0px 32px 0px; - gap: 8px; - } + border-right: 1px solid var(--line-color); n2-tree { .el-treenodes { @@ -168,10 +144,6 @@ html { } } - &:focus-within { - n2-tree {} - } - .node { display: grid; grid-template-columns: 40px min-content; diff --git a/static/js/tree.mjs b/static/js/tree.mjs index fc861e4..397dfc9 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -64,16 +64,61 @@ export class N2Tree extends CustomHTMLElement { this.tmpl.innerHTML = ` +
From 12f8c019f0bd0110bc8097685a6be1660af37b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 4 Jun 2026 08:42:54 +0200 Subject: [PATCH 15/91] Small UI changes --- static/css/notes2.css | 4 ++++ static/images/leaf.svg | 20 +++++++++------- static/js/app.mjs | 1 - static/js/page_node.mjs | 6 ++--- static/js/tree.mjs | 51 ++++++++++++++++++++++------------------- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/static/css/notes2.css b/static/css/notes2.css index 5a3e0dd..e1a6972 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -199,6 +199,10 @@ html { #main-page { display: contents; + &:focus-within { + background-color: #faf; + } + &.node { #page-node { display: contents; diff --git a/static/images/leaf.svg b/static/images/leaf.svg index 306a2a0..9d200c3 100644 --- a/static/images/leaf.svg +++ b/static/images/leaf.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="leaf.svg" - inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" + inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="11.17754" - inkscape:cx="8.0965937" - inkscape:cy="22.903072" - inkscape:window-width="1916" - inkscape:window-height="1161" - inkscape:window-x="0" - inkscape:window-y="18" + inkscape:zoom="31.614857" + inkscape:cx="5.0609117" + inkscape:cy="9.5524708" + inkscape:window-width="2190" + inkscape:window-height="1401" + inkscape:window-x="1463" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1" showgrid="false" />folder-open-outlinenotebook-outlinetext-box-outline_mbus.dispatch('SHOW_PAGE', { page: 'history' })) + this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' })) this.showMarkdown(true) }// }}} @@ -106,9 +106,9 @@ export class N2PageNodeUI extends CustomHTMLElement { }// }}} takeFocus() {// {{{ if (this.showMarkdown()) { - this.elNodeMarkdown.focus() + this.elNodeMarkdown.focus({ preventScroll: true }) } else - this.elNodeContent.focus() + this.elNodeContent.focus({ preventScroll: true }) }// }}} contentChanged(event) {//{{{ diff --git a/static/js/tree.mjs b/static/js/tree.mjs index 397dfc9..4e92021 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -64,8 +64,6 @@ export class N2Tree extends CustomHTMLElement { this.tmpl.innerHTML = ` -