From c97d93d0bce9e020ec355f109d7ac6a0904704be Mon Sep 17 00:00:00 2001 From: PMAC Date: Sat, 1 Aug 2026 20:34:31 -0500 Subject: [PATCH] Add a TELOS explainer popover to the dashboard The TELOS panel lists coded entries (P1, M1, ...) with nothing on the page explaining what TELOS is or what the codes mean. Add an accessible info popover on the TELOS heading (hover, tap, or keyboard focus) that defines TELOS, lists the section codes, notes the numbers are stable IDs rather than a ranking, and links to the framework. - No new dependencies; no data or config changes. - Telos parsing/label logic extracted to theme/telosCodes.ts (mirroring currentState.ts) and covered by tests/telosCodes.test.ts (bun test). - Row parser recognizes the documented TELOS code set (P/M/N/G/C/I/K), ignores non-codes, and normalizes ids to uppercase -- consistent with the labels and the popover legend. - A11y: the popover is linked to its trigger via aria-describedby, and each id cell gets an aria-label combining the code and its section name. --- .../theme/components/DaemonDashboard.vue | 45 ++++++++++++++---- cms/.vitepress/theme/telosCodes.ts | 47 +++++++++++++++++++ tests/telosCodes.test.ts | 42 +++++++++++++++++ 3 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 cms/.vitepress/theme/telosCodes.ts create mode 100644 tests/telosCodes.test.ts diff --git a/cms/.vitepress/theme/components/DaemonDashboard.vue b/cms/.vitepress/theme/components/DaemonDashboard.vue index 0166623..8383b32 100644 --- a/cms/.vitepress/theme/components/DaemonDashboard.vue +++ b/cms/.vitepress/theme/components/DaemonDashboard.vue @@ -89,12 +89,30 @@
03

TELOS

+ + + + TELOS · purpose / end goal + A machine-readable statement of purpose, from Daniel Miessler’s TELOS framework. + + P Problem + M Mission + N Narrative + G Goal + C Challenge + I Idea + K Metric / KPI + + Numbers are stable IDs, not a ranking — e.g. M1 answers P1. + Open the TELOS framework → + + {{ daemonData.telos?.length || 0 }}
- {{ extractItemId(item) }} - {{ extractItemText(item) }} + {{ extractTelosId(item) }} + {{ extractTelosText(item) }}
@@ -268,6 +286,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import daemonDataJson from '../../../public/daemon-data.json' import { selectHeroCurrentState } from '../currentState' +import { extractTelosId, extractTelosText, telosCodeLabel, telosAriaLabel } from '../telosCodes' const currentTime = ref(new Date()) const daemonData = ref(daemonDataJson) @@ -359,14 +378,7 @@ function formatDate(dateStr: string): string { return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) } -function extractItemId(item: string): string { - const match = item.match(/^([PMG]\d+)/) - return match ? match[1] : '---' -} -function extractItemText(item: string): string { - return item.replace(/^[PMG]\d+:\s*/, '') -} function extractConfidence(pred: string): string { const match = pred.match(/\(([^)]+)\)\s*$/) @@ -631,6 +643,21 @@ function extractProjectName(project: string): string { color: var(--ul-text); flex: 1; } .panel-meta { font-family: 'triplicate', monospace; font-size: 0.62rem; color: var(--ul-text-3); } +.telos-info { position: relative; display: inline-flex; } +.telos-info-btn { font-family: 'triplicate', monospace; font-size: 0.7rem; line-height: 1; color: var(--ul-text-3); background: none; border: none; padding: 0; margin: 0 0.2rem; cursor: help; transition: color .15s; } +.telos-info-btn:hover, .telos-info:focus-within .telos-info-btn { color: var(--ul-brand-light); } +.telos-info-pop { position: absolute; top: calc(100% + 9px); left: 0; z-index: 40; width: min(320px, 78vw); display: flex; flex-direction: column; gap: 0.5rem; padding: 0.85rem 0.95rem; background: var(--ul-bg-3); border: 1px solid var(--ul-border-3); box-shadow: 0 12px 34px #00000080, 0 0 0 1px var(--ul-brand-glow); opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-4px); transition: opacity .16s ease, transform .16s ease; text-align: left; max-height: min(68vh, 460px); overflow-y: auto; } +.telos-info:hover .telos-info-pop, .telos-info:focus-within .telos-info-pop { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); } +.telos-info-pop::before { content: ''; position: absolute; bottom: 100%; left: 12px; border: 6px solid transparent; border-bottom-color: var(--ul-bg-3); } +.tip-head { font-family: 'triplicate', monospace; font-size: 0.72rem; letter-spacing: 0.08em; color: var(--ul-text); text-transform: uppercase; } +.tip-head em { color: var(--ul-text-3); font-style: normal; text-transform: none; letter-spacing: 0; } +.tip-body { font-size: 0.8rem; line-height: 1.5; color: var(--ul-text-2); } +.tip-note { font-size: 0.72rem; line-height: 1.45; color: var(--ul-text-3); font-style: italic; } +.tip-legend { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.45rem 0; border-top: 1px solid var(--ul-border); border-bottom: 1px solid var(--ul-border); } +.tip-code { font-size: 0.78rem; color: var(--ul-text-2); display: flex; gap: 0.55rem; align-items: baseline; } +.tip-code b { font-family: 'triplicate', monospace; color: var(--ul-brand-light); min-width: 1.1em; } +.tip-link { font-family: 'triplicate', monospace; font-size: 0.72rem; color: var(--ul-brand-light); text-decoration: none; align-self: flex-start; } +.tip-link:hover { text-decoration: underline; } /* Boot cascade */ .boot { animation: boot 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; animation-delay: calc(var(--i) * 70ms); } diff --git a/cms/.vitepress/theme/telosCodes.ts b/cms/.vitepress/theme/telosCodes.ts new file mode 100644 index 0000000..a0fa659 --- /dev/null +++ b/cms/.vitepress/theme/telosCodes.ts @@ -0,0 +1,47 @@ +// TELOS entry codes. +// +// Entries in the `telos` array are written as ": text" — e.g. +// "P1: Not enough people understand ...". The letter is the TELOS section; the +// number is a stable identifier (M1 answers P1), NOT a priority ranking. +// +// Framework: https://github.com/danielmiessler/Telos +const TELOS_CODE_LABELS: Record = { + P: 'Problem', + M: 'Mission', + N: 'Narrative', + G: 'Goal', + C: 'Challenge', + I: 'Idea', + K: 'Metric (KPI)', +} + +// Only the recognised TELOS code letters are treated as codes — derived from the +// map above so the parser, the labels, and the popover legend can never drift. +const CODES = Object.keys(TELOS_CODE_LABELS).join('') // 'PMNGCIK' +const ID_RE = new RegExp(`^([${CODES}])(\\d+)`, 'i') +const PREFIX_RE = new RegExp(`^[${CODES}]\\d+:\\s*`, 'i') +const CODE_RE = new RegExp(`^([${CODES}])\\d+`, 'i') + +/** The "" id at the start of a telos entry (e.g. "P1"), uppercased; '---' when the leading code isn't a known TELOS code. */ +export function extractTelosId(item: string): string { + const m = item.match(ID_RE) + return m ? m[1].toUpperCase() + m[2] : '---' +} + +/** The entry text with a recognised ": " prefix removed. */ +export function extractTelosText(item: string): string { + return item.replace(PREFIX_RE, '') +} + +/** Friendly section name for a telos entry's code, or '' when the code isn't a known TELOS code. */ +export function telosCodeLabel(item: string): string { + const m = item.match(CODE_RE) + return m ? TELOS_CODE_LABELS[m[1].toUpperCase()] : '' +} + +/** Accessible label for the id cell: the id plus its section name when known (e.g. "P1, Problem"). */ +export function telosAriaLabel(item: string): string { + const id = extractTelosId(item) + const label = telosCodeLabel(item) + return label ? `${id}, ${label}` : id +} diff --git a/tests/telosCodes.test.ts b/tests/telosCodes.test.ts new file mode 100644 index 0000000..4b10d46 --- /dev/null +++ b/tests/telosCodes.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'bun:test' +import { extractTelosId, extractTelosText, telosCodeLabel, telosAriaLabel } from '../cms/.vitepress/theme/telosCodes' + +test('telosCodeLabel maps every known TELOS code to its section name', () => { + expect(telosCodeLabel('P1: a problem')).toBe('Problem') + expect(telosCodeLabel('M2: a mission')).toBe('Mission') + expect(telosCodeLabel('N1: a narrative')).toBe('Narrative') + expect(telosCodeLabel('G3: a goal')).toBe('Goal') + expect(telosCodeLabel('C4: a challenge')).toBe('Challenge') + expect(telosCodeLabel('I1: an idea')).toBe('Idea') + expect(telosCodeLabel('K2: a metric')).toBe('Metric (KPI)') +}) + +test('telosCodeLabel is case-insensitive on the code letter', () => { + expect(telosCodeLabel('p1: lowercase code')).toBe('Problem') +}) + +test('telosCodeLabel returns an empty string for unknown or missing codes', () => { + expect(telosCodeLabel('Z9: unrecognised code')).toBe('') + expect(telosCodeLabel('freeform entry, no code')).toBe('') +}) + +test('extractTelosId returns the id (uppercased) only for a known TELOS code', () => { + expect(extractTelosId('P1: a problem')).toBe('P1') + expect(extractTelosId('K12: a metric')).toBe('K12') + expect(extractTelosId('p1: lowercase')).toBe('P1') + expect(extractTelosId('Z9: unknown code')).toBe('---') + expect(extractTelosId('COVID19: not a code')).toBe('---') + expect(extractTelosId('freeform entry')).toBe('---') +}) + +test('extractTelosText strips only a recognised ": " prefix', () => { + expect(extractTelosText('M1: Own the system')).toBe('Own the system') + expect(extractTelosText('Z9: unknown code stays')).toBe('Z9: unknown code stays') + expect(extractTelosText('freeform entry')).toBe('freeform entry') +}) + +test('telosAriaLabel combines the id and section name, or falls back to the id', () => { + expect(telosAriaLabel('P1: a problem')).toBe('P1, Problem') + expect(telosAriaLabel('M2: a mission')).toBe('M2, Mission') + expect(telosAriaLabel('Z9: unknown')).toBe('---') +})