diff --git a/static/css/notes2.css b/static/css/notes2.css
index 7c2320f..8351858 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -52,51 +52,51 @@ html {
#tree {
grid-area: tree;
display: grid;
- background-color: #fafafa;
+ background-color: #ffffff;
color: #444;
z-index: 100;
- border-right: 1px solid #ddd;
-
- n2-tree {
- /*border: 2px solid #f8f8f8;*/
- padding: 16px 48px 16px 24px;
- }
-
- &:focus-within {
- n2-tree {
- /*
- border: 2px solid #fe5f55;
- */
- }
-
- }
-
+ border-right: 2px solid #ddd;
#logo {
display: grid;
- position: relative;
- justify-items: center;
- margin-top: 8px;
- margin-bottom: 8px;
- margin-left: 24px;
- margin-right: 24px;
+ grid-template-columns: min-content 1fr min-content;
+ align-items: center;
+ justify-items: start;
cursor: pointer;
+ padding: 16px;
+ border-bottom: 1px solid #ccc;
- img {
- width: 128px;
- left: -20px;
+ .el-search {
+ justify-self: end;
+ }
+ img:first-child {
+ height: 24px;
+ margin-right: 8px;
}
}
.icons {
display: flex;
justify-content: center;
- margin-bottom: 32px;
+ margin: 16px 0px 32px 0px;
gap: 8px;
}
+ n2-tree {
+ .el-treenodes {
+ margin: 32px;
+ }
+ }
+
+ &:focus-within {
+ n2-tree {
+ }
+
+ }
+
+
.node {
display: grid;
grid-template-columns: 40px min-content;
@@ -145,16 +145,6 @@ html {
}
}
-#tree-nodes {
- padding: 16px 32px;
- /*
- border-radius: 8px;
-*/
- /*
- box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
- */
-}
-
#crumbs {
grid-area: crumbs;
display: grid;
diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg
index d93f4ca..db06415 100644
--- a/static/images/collapsed.svg
+++ b/static/images/collapsed.svg
@@ -8,7 +8,7 @@
version="1.1"
id="svg1"
sodipodi:docname="collapsed.svg"
- inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
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"
@@ -23,13 +23,13 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
- inkscape:zoom="4.8373092"
- inkscape:cx="6.201795"
- inkscape:cy="-12.40359"
- inkscape:window-width="1916"
- inkscape:window-height="1161"
- inkscape:window-x="0"
- inkscape:window-y="18"
+ inkscape:zoom="19.349237"
+ inkscape:cx="11.809251"
+ inkscape:cy="6.3051583"
+ inkscape:window-width="1093"
+ inkscape:window-height="1401"
+ inkscape:window-x="2560"
+ inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="false" />
@@ -42,9 +42,13 @@
transform="translate(-102.39375,-146.31458)">
folder-outline
+
+ style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.330728;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
diff --git a/static/images/expanded.svg b/static/images/expanded.svg
index 017e8a4..9b420a8 100644
--- a/static/images/expanded.svg
+++ b/static/images/expanded.svg
@@ -8,7 +8,7 @@
version="1.1"
id="svg1"
sodipodi:docname="expanded.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="20.845374"
- inkscape:cy="26.929003"
- inkscape:window-width="1916"
- inkscape:window-height="1161"
- inkscape:window-x="0"
- inkscape:window-y="18"
+ inkscape:zoom="15.807429"
+ inkscape:cx="10.533022"
+ inkscape:cy="16.384701"
+ inkscape:window-width="1093"
+ inkscape:window-height="1401"
+ inkscape:window-x="1463"
+ inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />folder-openfolder-open-outline
+ style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
diff --git a/static/images/icon_settings.svg b/static/images/icon_settings.svg
new file mode 100644
index 0000000..1b692ad
--- /dev/null
+++ b/static/images/icon_settings.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/static/images/logo.svg b/static/images/logo.svg
index 3b5efa4..f294234 100644
--- a/static/images/logo.svg
+++ b/static/images/logo.svg
@@ -2,12 +2,12 @@
diff --git a/static/images/logo_small.svg b/static/images/logo_small.svg
new file mode 100644
index 0000000..cb83d39
--- /dev/null
+++ b/static/images/logo_small.svg
@@ -0,0 +1,63 @@
+
+
+
+
diff --git a/static/js/lib/css_colorize.mjs b/static/js/lib/css_colorize.mjs
new file mode 100644
index 0000000..f0fdb37
--- /dev/null
+++ b/static/js/lib/css_colorize.mjs
@@ -0,0 +1,207 @@
+export class Color {
+ constructor(r, g, b) { this.set(r, g, b); }
+ toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
+
+ set(r, g, b) {
+ this.r = this.clamp(r);
+ this.g = this.clamp(g);
+ this.b = this.clamp(b);
+ }
+
+ hueRotate(angle = 0) {
+ angle = angle / 180 * Math.PI;
+ let sin = Math.sin(angle);
+ let cos = Math.cos(angle);
+
+ this.multiply([
+ 0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
+ 0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
+ 0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
+ ]);
+ }
+
+ grayscale(value = 1) {
+ this.multiply([
+ 0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
+ 0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
+ ]);
+ }
+
+ sepia(value = 1) {
+ this.multiply([
+ 0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
+ 0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
+ 0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
+ ]);
+ }
+
+ saturate(value = 1) {
+ this.multiply([
+ 0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
+ 0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
+ ]);
+ }
+
+ multiply(matrix) {
+ let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
+ let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
+ let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
+ this.r = newR; this.g = newG; this.b = newB;
+ }
+
+ brightness(value = 1) { this.linear(value); }
+ contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
+
+ linear(slope = 1, intercept = 0) {
+ this.r = this.clamp(this.r * slope + intercept * 255);
+ this.g = this.clamp(this.g * slope + intercept * 255);
+ this.b = this.clamp(this.b * slope + intercept * 255);
+ }
+
+ invert(value = 1) {
+ this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
+ this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
+ this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
+ }
+
+ hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
+ let r = this.r / 255;
+ let g = this.g / 255;
+ let b = this.b / 255;
+ let max = Math.max(r, g, b);
+ let min = Math.min(r, g, b);
+ let h, s, l = (max + min) / 2;
+
+ if (max === min) {
+ h = s = 0;
+ } else {
+ let d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ } h /= 6;
+ }
+
+ return {
+ h: h * 100,
+ s: s * 100,
+ l: l * 100
+ };
+ }
+
+ clamp(value) {
+ if (value > 255) { value = 255; }
+ else if (value < 0) { value = 0; }
+ return value;
+ }
+}
+
+export class Solver {
+ constructor(target) {
+ this.target = target;
+ this.targetHSL = target.hsl();
+ this.reusedColor = new Color(0, 0, 0); // Object pool
+ }
+
+ solve() {
+ let result = this.solveNarrow(this.solveWide());
+ return {
+ values: result.values,
+ loss: result.loss,
+ filter: this.css(result.values)
+ };
+ }
+
+ solveWide() {
+ const A = 5;
+ const c = 15;
+ const a = [60, 180, 18000, 600, 1.2, 1.2];
+
+ let best = { loss: Infinity };
+ for (let i = 0; best.loss > 25 && i < 3; i++) {
+ let initial = [50, 20, 3750, 50, 100, 100];
+ let result = this.spsa(A, a, c, initial, 1000);
+ if (result.loss < best.loss) { best = result; }
+ } return best;
+ }
+
+ solveNarrow(wide) {
+ const A = wide.loss;
+ const c = 2;
+ const A1 = A + 1;
+ const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
+ return this.spsa(A, a, c, wide.values, 500);
+ }
+
+ spsa(A, a, c, values, iters) {
+ const alpha = 1;
+ const gamma = 0.16666666666666666;
+
+ let best = null;
+ let bestLoss = Infinity;
+ let deltas = new Array(6);
+ let highArgs = new Array(6);
+ let lowArgs = new Array(6);
+
+ for (let k = 0; k < iters; k++) {
+ let ck = c / Math.pow(k + 1, gamma);
+ for (let i = 0; i < 6; i++) {
+ deltas[i] = Math.random() > 0.5 ? 1 : -1;
+ highArgs[i] = values[i] + ck * deltas[i];
+ lowArgs[i] = values[i] - ck * deltas[i];
+ }
+
+ let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
+ for (let i = 0; i < 6; i++) {
+ let g = lossDiff / (2 * ck) * deltas[i];
+ let ak = a[i] / Math.pow(A + k + 1, alpha);
+ values[i] = fix(values[i] - ak * g, i);
+ }
+
+ let loss = this.loss(values);
+ if (loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
+ } return { values: best, loss: bestLoss };
+
+ function fix(value, idx) {
+ let max = 100;
+ if (idx === 2 /* saturate */) { max = 7500; }
+ else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
+
+ if (idx === 3 /* hue-rotate */) {
+ if (value > max) { value = value % max; }
+ else if (value < 0) { value = max + value % max; }
+ } else if (value < 0) { value = 0; }
+ else if (value > max) { value = max; }
+ return value;
+ }
+ }
+
+ loss(filters) { // Argument is array of percentages.
+ let color = this.reusedColor;
+ color.set(0, 0, 0);
+
+ color.invert(filters[0] / 100);
+ color.sepia(filters[1] / 100);
+ color.saturate(filters[2] / 100);
+ color.hueRotate(filters[3] * 3.6);
+ color.brightness(filters[4] / 100);
+ color.contrast(filters[5] / 100);
+
+ let colorHSL = color.hsl();
+ return Math.abs(color.r - this.target.r)
+ + Math.abs(color.g - this.target.g)
+ + Math.abs(color.b - this.target.b)
+ + Math.abs(colorHSL.h - this.targetHSL.h)
+ + Math.abs(colorHSL.s - this.targetHSL.s)
+ + Math.abs(colorHSL.l - this.targetHSL.l);
+ }
+
+ css(filters) {
+ function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
+ return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%)`;
+ }
+}
diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs
index ca85d3e..5c9c0ff 100644
--- a/static/js/marked_position.mjs
+++ b/static/js/marked_position.mjs
@@ -110,12 +110,12 @@ export class MarkedPosition {
renderer: {
heading(token) {
const content = this.parser.parseInline(token.tokens)
- return `${content}\n`
+ return `${content}\n`
},
paragraph(token) {
const content = this.parser.parseInline(token.tokens)
- return `${content}
\n`
+ return `${content}
\n`
},
list(token) {
@@ -134,7 +134,7 @@ export class MarkedPosition {
},
listitem(token) {
- return `${this.parser.parse(token.tokens)}\n`
+ return `${this.parser.parse(token.tokens)}\n`
},
code(token) {
@@ -143,12 +143,12 @@ export class MarkedPosition {
const code = token.text.replace(other.endingNewline, '') + '\n'
if (!langString) {
- return ``
+ return ``
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '
\n'
}
- return `'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -157,7 +157,7 @@ export class MarkedPosition {
blockquote(token) {
const body = this.parser.parse(token.tokens)
- return `\n${body}
\n`
+ return `\n${body}
\n`
},
html(token) {
@@ -169,11 +169,11 @@ export class MarkedPosition {
},
hr(token) {
- return `
\n`
+ return `
\n`
},
checkbox(token) {
- return ` '
},
@@ -218,7 +218,7 @@ export class MarkedPosition {
if (token.tokens.length > 0) {
const start = token.tokens[0].position.start.offset
const end = token.tokens[0].position.end.offset
- ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
+ ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
}
const content = this.parser.parseInline(token.tokens);
@@ -230,23 +230,23 @@ export class MarkedPosition {
},
strong(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
em(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
codespan(token) {
- return `${escapeHtmlEntities(token.text, true)}`
+ return `${escapeHtmlEntities(token.text, true)}`
},
br(token) {
- return `
`
+ return `
`
},
del(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
link(token) {
@@ -256,7 +256,7 @@ export class MarkedPosition {
return text
}
token.href = cleanHref
- let out = '
-
+
+
+

+
`
@@ -31,7 +36,7 @@ export class N2Tree extends CustomHTMLElement {
this.elSync.addEventListener('click', () => _sync.run())
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
- _mbus.subscribe('NODE_MODIFIED', ({ detail })=>{
+ _mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
const node = detail.data.node
const treenode = this.treeNodeComponents[node.get('UUID')]
@@ -43,6 +48,12 @@ export class N2Tree extends CustomHTMLElement {
})
this.populateFirstLevel()
+
+ /* XXX - set color */
+ let color = new Color(255, 96, 80)
+ let solver = new Solver(color)
+ let result = solver.solve()
+ this.elSettings.style.filter = result.filter
}// }}}
render() {// {{{
if (this.rendered)