added symbols for later breakout groups to edit trackpoints
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,51 @@
|
|||||||
|
(function (ns) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const entries = Object.freeze([
|
||||||
|
['🍎', 'Apfel'],
|
||||||
|
['🔑', 'Schlüssel'],
|
||||||
|
['🔔', 'Glocke'],
|
||||||
|
['📘', 'Buch'],
|
||||||
|
['🌻', 'Sonnenblume'],
|
||||||
|
['☂️', 'Regenschirm'],
|
||||||
|
['⚽', 'Fußball'],
|
||||||
|
['☕', 'Tasse']
|
||||||
|
].map(([unicode, label]) => Object.freeze({
|
||||||
|
unicode,
|
||||||
|
label,
|
||||||
|
imageUrl: `/images/group-symbols/${unicode}.png`
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Presentation selectors must not create different symbols for the same object.
|
||||||
|
function normalizeUnicode(value) {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
return value.normalize('NFC').replaceAll(/[\uFE0E\uFE0F]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const byUnicode = new Map(entries.map(entry => [normalizeUnicode(entry.unicode), entry]));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only catalogue of the eight approved group-code illustrations.
|
||||||
|
* Loading this file only registers the class; it does not create UI or sessions.
|
||||||
|
*/
|
||||||
|
class GroupSymbols {
|
||||||
|
static list() {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the immutable motif descriptor, or null for an unknown symbol. */
|
||||||
|
static get(unicode) {
|
||||||
|
return byUnicode.get(normalizeUnicode(unicode)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static has(unicode) {
|
||||||
|
return this.get(unicode) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static imageUrl(unicode) {
|
||||||
|
return this.get(unicode)?.imageUrl ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ns.GroupSymbols = GroupSymbols;
|
||||||
|
}(globalThis.Wegwichtel ??= {}));
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const root = path.resolve(import.meta.dirname, '..');
|
||||||
|
|
||||||
|
async function catalogue(namespace) {
|
||||||
|
const filename = path.join(root, 'public/js/group-symbols.js');
|
||||||
|
const context = { Wegwichtel: namespace };
|
||||||
|
context.globalThis = context;
|
||||||
|
vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename });
|
||||||
|
return context.Wegwichtel.GroupSymbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('group motifs resolve all eight approved Unicode symbols to existing PNG assets', async () => {
|
||||||
|
const existing = { Config: { apiBase: '/api' } };
|
||||||
|
const symbols = await catalogue(existing);
|
||||||
|
assert.equal(existing.Config.apiBase, '/api');
|
||||||
|
const expected = ['🍎', '🔑', '🔔', '📘', '🌻', '☂️', '⚽', '☕'];
|
||||||
|
assert.deepEqual(Array.from(symbols.list(), entry => entry.unicode), expected);
|
||||||
|
assert.equal(new Set(symbols.list().map(entry => entry.unicode)).size, 8);
|
||||||
|
for (const entry of symbols.list()) {
|
||||||
|
assert.equal(symbols.get(entry.unicode), entry);
|
||||||
|
assert.equal(symbols.has(entry.unicode), true);
|
||||||
|
assert.equal(symbols.imageUrl(entry.unicode), entry.imageUrl);
|
||||||
|
assert.equal(entry.imageUrl, `/images/group-symbols/${entry.unicode}.png`);
|
||||||
|
assert.ok(entry.label.length > 0);
|
||||||
|
const png = await fs.readFile(path.join(root, 'public', entry.imageUrl));
|
||||||
|
assert.deepEqual(png.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
||||||
|
assert.equal(png[25], 6, 'each motif must be an RGBA PNG');
|
||||||
|
assert.equal(Object.isFrozen(entry), true);
|
||||||
|
}
|
||||||
|
assert.equal(Object.isFrozen(symbols.list()), true);
|
||||||
|
assert.throws(() => { symbols.list()[0].label = 'changed'; }, TypeError);
|
||||||
|
assert.throws(() => symbols.list().push({}), { name: 'TypeError' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unicode presentation selectors resolve to one motif and invalid inputs stay unknown', async () => {
|
||||||
|
const symbols = await catalogue();
|
||||||
|
for (const base of ['☂', '⚽', '☕']) {
|
||||||
|
assert.equal(symbols.get(base), symbols.get(`${base}\uFE0F`));
|
||||||
|
assert.equal(symbols.get(base), symbols.get(`${base}\uFE0E`));
|
||||||
|
}
|
||||||
|
for (const value of ['', '🍎🔑', 'unknown', ' 🍎 ', null, undefined, 42, {}]) {
|
||||||
|
assert.equal(symbols.get(value), null);
|
||||||
|
assert.equal(symbols.imageUrl(value), null);
|
||||||
|
assert.equal(symbols.has(value), false);
|
||||||
|
}
|
||||||
|
});
|
||||||