1/**
2 * Periodic Table of Commands
3 * Clean grid visualization showing all commands organized by category
4 * Hover tooltips show description and relationships inline.
5 */
6
7import { commandCategories, commandRelationships, betaCommands } from '../data.js';
8
9const categoryColors = {
10 create: { bg: 'var(--cat-create-bg)', border: 'var(--cat-create-border)', text: 'var(--cat-create-text)' },
11 evaluate: { bg: 'var(--cat-evaluate-bg)', border: 'var(--cat-evaluate-border)', text: 'var(--cat-evaluate-text)' },
12 refine: { bg: 'var(--cat-refine-bg)', border: 'var(--cat-refine-border)', text: 'var(--cat-refine-text)' },
13 simplify: { bg: 'var(--cat-simplify-bg)', border: 'var(--cat-simplify-border)', text: 'var(--cat-simplify-text)' },
14 harden: { bg: 'var(--cat-harden-bg)', border: 'var(--cat-harden-border)', text: 'var(--cat-harden-text)' },
15 system: { bg: 'var(--cat-system-bg)', border: 'var(--cat-system-border)', text: 'var(--cat-system-text)' }
16};
17
18const categoryLabels = {
19 create: 'Create',
20 evaluate: 'Evaluate',
21 refine: 'Refine',
22 simplify: 'Simplify',
23 harden: 'Harden',
24 system: 'System'
25};
26
27const commandSymbols = {
28 'shape': 'Sh',
29 'impeccable craft': 'Ic',
30 'impeccable': 'Im',
31 'overdrive': 'Od',
32 'critique': 'Cr',
33 'audit': 'Au',
34 'typeset': 'Ty',
35 'layout': 'La',
36 'colorize': 'Co',
37 'animate': 'An',
38 'delight': 'De',
39 'bolder': 'Bo',
40 'quieter': 'Qu',
41 'distill': 'Di',
42 'clarify': 'Cl',
43 'adapt': 'Ad',
44 'polish': 'Po',
45 'optimize': 'Op',
46 'harden': 'Ha',
47 'impeccable teach': 'It',
48 'impeccable extract': 'Ie'
49};
50
51const commandNumbers = {
52 'shape': 0,
53 'impeccable craft': 1, 'impeccable': 2, 'overdrive': 3,
54 'critique': 4, 'audit': 5,
55 'typeset': 6, 'layout': 7, 'colorize': 8, 'animate': 9,
56 'delight': 10, 'bolder': 11, 'quieter': 12,
57 'distill': 13, 'clarify': 14, 'adapt': 15,
58 'polish': 16, 'optimize': 17, 'harden': 18,
59 'impeccable teach': 19, 'impeccable extract': 20
60};
61
62// Map sub-commands to their display label and scroll target
63const commandDisplay = {
64 'impeccable craft': { label: '/impeccable craft', scrollTo: 'impeccable' },
65 'impeccable teach': { label: '/impeccable teach', scrollTo: 'impeccable' },
66 'impeccable extract': { label: '/impeccable extract', scrollTo: 'impeccable' },
67};
68
69export class PeriodicTable {
70 constructor(container) {
71 this.container = container;
72 this.activeTooltip = null;
73 this.activeElement = null;
74 this.init();
75 }
76
77 init() {
78 this.container.innerHTML = '';
79 this.container.style.cssText = `
80 display: flex;
81 flex-direction: column;
82 gap: 16px;
83 padding: 20px;
84 height: 100%;
85 box-sizing: border-box;
86 position: relative;
87 `;
88
89 this.renderTable();
90 }
91
92 renderTable() {
93 const groups = {};
94 Object.entries(commandCategories).forEach(([cmd, cat]) => {
95 if (!groups[cat]) groups[cat] = [];
96 groups[cat].push(cmd);
97 });
98
99 const categoryOrder = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
100
101 const grid = document.createElement('div');
102 grid.style.cssText = `
103 display: grid;
104 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
105 gap: 16px;
106 flex: 1;
107 `;
108
109 categoryOrder.forEach(cat => {
110 const commands = groups[cat];
111 if (!commands) return;
112 const group = this.createCategoryGroup(cat, commands);
113 grid.appendChild(group);
114 });
115
116 this.container.appendChild(grid);
117 }
118
119 showTooltip(el, cmd) {
120 this.hideTooltip();
121
122 const rel = commandRelationships[cmd] || {};
123 const toArray = (val) => {
124 if (!val) return [];
125 if (Array.isArray(val)) return val;
126 return [val];
127 };
128
129 const pairs = toArray(rel.pairs);
130 const leadsTo = toArray(rel.leadsTo);
131 const combinesWith = toArray(rel.combinesWith);
132
133 // Build relationships line
134 let relParts = [];
135 if (pairs.length > 0) relParts.push(`pairs with ${pairs.map(p => '/' + p).join(', ')}`);
136 if (combinesWith.length > 0) relParts.push(`+ ${combinesWith.map(p => '/' + p).join(', ')}`);
137 if (leadsTo.length > 0) relParts.push(`then ${leadsTo.map(p => '/' + p).join(', ')}`);
138
139 // Strip category prefix from flow for cleaner display
140 const flow = (rel.flow || '').replace(/^[^:]+:\s*/, '');
141
142 const tooltip = document.createElement('div');
143 tooltip.className = 'ptable-tooltip';
144 tooltip.style.cssText = `
145 position: absolute;
146 z-index: 20;
147 background: var(--color-paper);
148 border: 1px solid var(--color-mist);
149 border-radius: 6px;
150 padding: 10px 14px;
151 box-shadow: 0 8px 24px -4px rgba(0,0,0,0.12);
152 pointer-events: none;
153 max-width: 280px;
154 opacity: 0;
155 transition: opacity 0.15s ease;
156 `;
157
158 tooltip.innerHTML = `
159 <div style="font-family: var(--font-body); font-size: 13px; color: var(--color-charcoal); line-height: 1.4; margin-bottom: ${relParts.length ? '6px' : '0'};">${flow}</div>
160 ${relParts.length ? `<div style="font-family: var(--font-mono); font-size: 11px; color: var(--color-ash); line-height: 1.4;">${relParts.join(' · ')}</div>` : ''}
161 `;
162
163 this.container.appendChild(tooltip);
164
165 // Position relative to element
166 const elRect = el.getBoundingClientRect();
167 const containerRect = this.container.getBoundingClientRect();
168
169 const left = elRect.left - containerRect.left;
170 const top = elRect.bottom - containerRect.top + 6;
171
172 tooltip.style.left = `${Math.min(left, containerRect.width - 290)}px`;
173 tooltip.style.top = `${top}px`;
174
175 // Fade in
176 requestAnimationFrame(() => { tooltip.style.opacity = '1'; });
177
178 this.activeTooltip = tooltip;
179 }
180
181 hideTooltip() {
182 if (this.activeTooltip) {
183 this.activeTooltip.remove();
184 this.activeTooltip = null;
185 }
186 }
187
188 createCategoryGroup(category, commands) {
189 const colors = categoryColors[category];
190
191 const group = document.createElement('div');
192 group.style.cssText = `display: flex; flex-direction: column; gap: 6px;`;
193
194 const label = document.createElement('div');
195 label.style.cssText = `
196 font-family: var(--font-body);
197 font-size: 10px;
198 font-weight: 500;
199 text-transform: uppercase;
200 letter-spacing: 0.05em;
201 color: ${colors.text};
202 padding-left: 2px;
203 `;
204 label.textContent = categoryLabels[category];
205 group.appendChild(label);
206
207 const row = document.createElement('div');
208 row.style.cssText = `display: flex; flex-wrap: wrap; gap: 6px;`;
209
210 commands.forEach(cmd => {
211 const element = this.createElement(cmd, category);
212 row.appendChild(element);
213 });
214
215 group.appendChild(row);
216 return group;
217 }
218
219 createElement(cmd, category) {
220 const colors = categoryColors[category];
221 const display = commandDisplay[cmd];
222
223 const el = document.createElement('button');
224 el.type = 'button';
225 el.setAttribute('aria-label', `/${cmd} command - ${categoryLabels[category]}`);
226 el.style.cssText = `
227 width: 56px;
228 height: 64px;
229 background: ${colors.bg};
230 border: 1.5px solid ${colors.border};
231 border-radius: 5px;
232 display: flex;
233 flex-direction: column;
234 align-items: center;
235 justify-content: center;
236 cursor: pointer;
237 transition: transform 0.15s ease, box-shadow 0.15s ease;
238 position: relative;
239 font-family: inherit;
240 padding: 0;
241 `;
242
243 // Atomic number
244 const number = document.createElement('div');
245 number.style.cssText = `
246 position: absolute;
247 top: 3px;
248 left: 5px;
249 font-family: var(--font-mono);
250 font-size: 7px;
251 color: ${colors.text};
252 opacity: 0.5;
253 `;
254 number.textContent = commandNumbers[cmd];
255 el.appendChild(number);
256
257 // Symbol
258 const symbol = document.createElement('div');
259 symbol.style.cssText = `
260 font-family: var(--font-display);
261 font-size: 20px;
262 font-weight: 500;
263 color: ${colors.text};
264 line-height: 1;
265 `;
266 symbol.textContent = commandSymbols[cmd];
267 el.appendChild(symbol);
268
269 // Command name
270 const name = document.createElement('div');
271 name.style.cssText = `
272 font-family: var(--font-mono);
273 font-size: ${display ? '6.5px' : '8px'};
274 color: ${colors.text};
275 opacity: 0.7;
276 margin-top: 3px;
277 text-align: center;
278 max-width: 52px;
279 line-height: 1.3;
280 ${display ? '' : 'white-space: nowrap;'}
281 `;
282 name.textContent = display ? display.label : `/${cmd}`;
283 el.appendChild(name);
284
285 // Beta badge
286 if (betaCommands.includes(cmd)) {
287 const badge = document.createElement('div');
288 badge.style.cssText = `
289 position: absolute;
290 top: 2px;
291 right: 3px;
292 font-family: var(--font-mono);
293 font-size: 5px;
294 letter-spacing: 0.05em;
295 color: ${colors.text};
296 opacity: 0.45;
297 text-transform: uppercase;
298 `;
299 badge.textContent = 'β';
300 el.appendChild(badge);
301 }
302
303 // Hover/focus: show tooltip
304 const activate = () => {
305 el.style.transform = 'translateY(-2px)';
306 el.style.boxShadow = `0 4px 12px ${colors.border}40`;
307 this.showTooltip(el, cmd);
308
309 if (this.activeElement && this.activeElement !== el) {
310 this.activeElement.style.transform = 'translateY(0)';
311 this.activeElement.style.boxShadow = 'none';
312 }
313 this.activeElement = el;
314 };
315
316 const deactivate = () => {
317 el.style.transform = 'translateY(0)';
318 el.style.boxShadow = 'none';
319 this.hideTooltip();
320 };
321
322 el.addEventListener('mouseenter', activate);
323 el.addEventListener('mouseleave', deactivate);
324 el.addEventListener('focus', activate);
325 el.addEventListener('blur', deactivate);
326
327 el.addEventListener('touchstart', (e) => {
328 e.preventDefault();
329 activate();
330 }, { passive: false });
331
332 el.addEventListener('click', () => {
333 activate();
334 const scrollTarget = display ? display.scrollTo : cmd;
335
336 // Navigate the fisheye scroller to this command
337 const fisheyeList = document.getElementById('fisheye-list');
338 if (fisheyeList) {
339 const items = [...fisheyeList.querySelectorAll('.fisheye-item')];
340 const idx = items.findIndex(item => item.dataset.id === scrollTarget);
341 if (idx >= 0 && fisheyeList._scrollToCommand) {
342 // Scroll the commands section into view first
343 const section = document.querySelector('.commands-subsection');
344 if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' });
345 fisheyeList._scrollToCommand(idx);
346 return;
347 }
348 }
349
350 // Fallback: scroll to the spread element
351 const target = document.getElementById(`cmd-${scrollTarget}`);
352 if (target) {
353 target.scrollIntoView({ behavior: 'smooth', block: 'center' });
354 }
355 });
356
357 return el;
358 }
359}
360
361export function initFrameworkViz() {
362 const container = document.getElementById('framework-viz-container');
363 if (container) {
364 new PeriodicTable(container);
365 }
366}