+ // v-tree-glow-1-v0: a legend word opens its light, the same way "go to" does. + function bindLegendRow(row, id) { + row.setAttribute('role', 'button'); + row.setAttribute('tabindex', '0'); + row.setAttribute('aria-label', 'go to ' + goWord(id)); + row.addEventListener('click', function (e) { + if (e && e.preventDefault) e.preventDefault(); + if (e && e.stopPropagation) e.stopPropagation(); + goToLumino(id); + }); + row.addEventListener('keydown', function (e) { + var k = e && e.key; + if (k !== 'Enter' && k !== ' ') return; + if (e.preventDefault) e.preventDefault(); + goToLumino(id); + }); + } + function ensureSkyLegend() { // Tiny quiet chip: which current color links to what. // Not a wall. Not a second you-are-here. Words help. @@ -825,7 +843,9 @@ if (!doors.length) return; var list = document.createElement('div'); list.id = 'lumino-legend'; - list.setAttribute('aria-hidden', 'true'); + // v-tree-glow-1-v0: the words are quiet doors now (a light may orbit off a phone + // screen for most of a minute). Was aria-hidden; now each row says where it goes. + list.setAttribute('role', 'group'); for (var i = 0; i < doors.length; i++) { if (doors[i].hidden || doors[i].classList.contains('is-held')) continue; var id = doors[i].getAttribute('data-garden-lumino') || @@ -845,6 +865,7 @@ word.textContent = goWord(id); row.appendChild(dot); row.appendChild(word); + if (id) bindLegendRow(row, id); // v-tree-glow-1-v0 list.appendChild(row); } if (!list.childNodes.length) return; diff --git a/docs/modules/tree-glow-1.test.js b/docs/modules/tree-glow-1.test.js new file mode 100644 index 0000000..c3fd318 --- /dev/null +++ b/docs/modules/tree-glow-1.test.js @@ -0,0 +1,278 @@ +#!/usr/bin/env node +// v-tree-glow-1-v0: every light in reach. theLatticeTree only; no fl-connect.js, no +// FreeLattice app.html. Guards: (A) the "Use a different address" field stays inside its +// card on a phone, (B) Settings lines fade before "the garden" pill, (C) the Nursery's +// last card clears the galaxy hop, (D) the Gathering's Find local minds is not under the +// promise band on wide screens, (E) legend words open their lights, and the room-page +// reset no longer outweighs each card's own padding. No network. No kitchen. +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var vm = require('vm'); +var assert = require('assert'); + +var css = fs.readFileSync(path.join(__dirname, 'garden-rooms.css'), 'utf8'); +var js = fs.readFileSync(path.join(__dirname, 'garden-rooms.js'), 'utf8'); + +// Static: the reset weighs nothing now; the old heavy selector is gone from live rules. +assert.ok(/:where\(html\.room-page\) \*,\n:where\(html\.room-page\) \*::before,\n:where\(html\.room-page\) \*::after \{ box-sizing: border-box; margin: 0; padding: 0; \}/.test(css), 'room-page reset is :where (zero weight)'); +assert.ok(!/^html\.room-page \*,$/m.test(css), 'the heavy reset selector is not a live rule any more'); +var glow = css.slice(css.indexOf('v-tree-glow-1-v0: every light in reach')); +assert.ok(glow.length > 200, 'glow block at the end of garden-rooms.css'); +assert.ok(/@media \(max-width: 480px\) \{\s*\.flc-picker-row input \{[^}]*width: 100%;[^}]*max-width: 100%;[^}]*min-width: 0;/.test(glow), 'A: field stays inside the card'); +assert.ok(/#place-veil\.is-settings::after \{[^}]*position: sticky;[^}]*pointer-events: none;/.test(glow), 'B: soft band before the pill, never takes a tap'); +assert.ok(/html\.room-page \.room-nursery \{\s*padding-bottom: 7\.5rem;/.test(glow), 'C: Nursery clears the hop'); +assert.ok(/@media \(min-width: 641px\) \{\s*#place-veil\.is-core \.core-promise \{\s*top: 14\.6rem;/.test(glow), 'D: promise band under Find local minds'); +assert.ok(/#lumino-legend \.lumino-legend-row\[data-lumino-row\] \{[^}]*pointer-events: auto;/.test(glow), 'E: legend rows take a tap'); +assert.ok(/v-tree-glow-1-v0/.test(js) && /function bindLegendRow\(row, id\)/.test(js), 'E: legend rows are bound'); +assert.ok(!/confirm\(/.test(js), 'no confirm()'); +assert.ok(!/innerHTML/.test(js.slice(js.indexOf('function bindLegendRow'), js.indexOf('function ensureSkyLegend'))), 'no innerHTML in the new code'); + +// Runtime (vm, fake DOM): tapping a legend word opens its light. +function El(tag) { + this.tagName = String(tag || 'div').toUpperCase(); + this.tag = tag; + this.id = ''; + this.className = ''; + this.textContent = ''; + this.hidden = false; + this.attrs = {}; + this.children = []; + this.childNodes = this.children; + this.parent = null; + this.style = { + background: '', + boxShadow: '', + left: '', + top: '', + _props: {}, + setProperty: function (k, v) { this._props[k] = String(v); }, + getPropertyValue: function (k) { return this._props[k] || ''; } + }; + var self = this; + this.classList = { + add: function (name) { + if ((' ' + self.className + ' ').indexOf(' ' + name + ' ') === -1) { + self.className = (self.className ? self.className + ' ' : '') + name; + } + }, + remove: function (name) { + self.className = String(self.className || '') + .split(/\s+/) + .filter(function (c) { return c && c !== name; }) + .join(' '); + }, + contains: function (name) { + return (' ' + self.className + ' ').indexOf(' ' + name + ' ') !== -1; + } + }; + this.setAttribute = function (name, val) { + var s = String(val); + this.attrs[name] = s; + if (name === 'id') this.id = s; + if (name === 'class') this.className = s; + if (name === 'hidden') this.hidden = true; + }; + this.getAttribute = function (name) { + if (name === 'id') return this.id || null; + if (Object.prototype.hasOwnProperty.call(this.attrs, name)) return this.attrs[name]; + return null; + }; + this.removeAttribute = function (name) { + delete this.attrs[name]; + if (name === 'id') this.id = ''; + }; + this.appendChild = function (child) { + this.children.push(child); + child.parent = this; + return child; + }; + this.querySelector = function (sel) { return queryOne(this, sel); }; + this.querySelectorAll = function (sel) { return queryAll(this, sel); }; + this._on = {}; + this.addEventListener = function (t, fn) { (this._on[t] = this._on[t] || []).push(fn); }; + this.fire = function (t, ev) { (this._on[t] || []).forEach(function (fn) { fn(ev || { preventDefault: function () {}, stopPropagation: function () {} }); }); }; + this.contains = function (node) { + if (node === this) return true; + for (var i = 0; i < this.children.length; i++) { + if (this.children[i].contains && this.children[i].contains(node)) return true; + } + return false; + }; +} + +function walk(root, out) { + out.push(root); + for (var i = 0; i < (root.children || []).length; i++) walk(root.children[i], out); + return out; +} + +function matchOne(el, raw) { + var sel = String(raw || '').trim(); + if (!sel || !el) return false; + if (sel.charAt(0) === '#') return el.id === sel.slice(1); + if (sel.charAt(0) === '.') { + return (' ' + (el.className || '') + ' ').indexOf(' ' + sel.slice(1) + ' ') !== -1; + } + var attrEq = sel.match(/^\[([^\]]+?)="([^"]*)"\]$/); + if (attrEq) return el.getAttribute(attrEq[1]) === attrEq[2]; + var attr = sel.match(/^\[([^\]]+)\]$/); + if (attr) return el.getAttribute(attr[1]) != null; + return (el.tag || '').toLowerCase() === sel.toLowerCase(); +} + +function matchCompound(el, compound) { + var parts = String(compound || '').trim().split(/(?=[.#\[])/); + if (!parts.length || (parts.length === 1 && !parts[0])) return false; + if (/^[a-zA-Z]/.test(parts[0])) { + if ((el.tag || '').toLowerCase() !== parts[0].toLowerCase()) return false; + parts = parts.slice(1); + } + for (var i = 0; i < parts.length; i++) { + if (parts[i] && !matchOne(el, parts[i])) return false; + } + return true; +} + +function queryAll(root, selector) { + var groups = String(selector || '').split(','); + var found = []; + var nodes = walk(root, []); + for (var g = 0; g < groups.length; g++) { + var sel = groups[g].trim(); + var bits = sel.split(/\s+/); + for (var n = 0; n < nodes.length; n++) { + var el = nodes[n]; + if (el === root && bits.length === 1 && matchCompound(el, bits[0])) { + if (found.indexOf(el) === -1) found.push(el); + continue; + } + if (bits.length === 1) { + if (el !== root && matchCompound(el, bits[0]) && found.indexOf(el) === -1) found.push(el); + } else if (bits.length === 2) { + if (el !== root && matchCompound(el, bits[1]))