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('---') +})