Skip to content

Commit 4264ae6

Browse files
author
DavidQ
committed
Harden tools template into official first-class tool starter - PR_26126_063-tool-template-hardening
1 parent c90cb73 commit 4264ae6

14 files changed

Lines changed: 779 additions & 0 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# PR_26126_063 Tool Template Hardening
2+
3+
Scope:
4+
- Updated tools/templates only by adding the official first-class tool starter surface.
5+
- Added required review reports under docs/dev/reports.
6+
- Preserved existing tools/templates/starter-project-template and tools/templates/vector-native-arcade content because runtime/tests still reference those folders.
7+
8+
Current tools/templates review:
9+
- Existing starter-project-template is a shared project manifest starter, not a first-class tool starter.
10+
- Existing vector-native-arcade is a project/game starter, not a first-class tool starter.
11+
- Existing vector-native-arcade paths are referenced by tools/shared helpers and tests, so they were left untouched.
12+
13+
Added official starter:
14+
- tools/templates/README.md identifies first-class-tool-starter as the official starter pattern.
15+
- tools/templates/first-class-tool-starter/index.html is a clean shell with external CSS and external module JavaScript only.
16+
- tools/templates/first-class-tool-starter/styles/toolStarter.css contains all styles.
17+
- tools/templates/first-class-tool-starter/js/bootstrap.js is the small wiring entry.
18+
- tools/templates/first-class-tool-starter/js/ToolStarterApp.js is coordinator-only.
19+
- tools/templates/first-class-tool-starter/js/controls/AccordionSection.js demonstrates reusable accordion behavior.
20+
- One UI control/section class exists per file for action nav, source input, preview, status log, and inspector.
21+
- tools/templates/first-class-tool-starter/js/services/ToolStateSerializer.js keeps serialization separate from controls and app wiring.
22+
23+
Validation:
24+
- Playwright impacted: No
25+
- No Playwright impact. This PR is template/starter-only and does not modify active tool runtime, workspace/toolState flows, samples, schemas, or roadmap.
26+
- Syntax checked all first-class-tool-starter JavaScript files with node --check.
27+
- Verified template HTML has no inline script blocks, style blocks, or inline event handlers.
28+
- Verified no tracked roadmap, sample, start_of_day, package, src, games, or tools/shared files changed.
29+
- npm run test:workspace-v2 was not run because there is no active runtime/UI behavior impact.
30+
31+
Manual test notes:
32+
- Open tools/templates/first-class-tool-starter/index.html in a browser or local server.
33+
- Confirm accordion sections expand/collapse, Run is disabled until Source value is provided, and Status/Output Summary update after Run.
34+
- Confirm new tool authors can follow tools/templates/first-class-tool-starter/README.md to create a first-class tool.
35+
36+
Out of scope:
37+
- Did not modify live tools outside tools/templates.
38+
- Did not modify samples, schemas, roadmap, tools/shared, or start_of_day folders.

tools/templates/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Tools Templates
2+
3+
`tools/templates` contains reusable starter content. The official starter for new first-class tools is:
4+
5+
`tools/templates/first-class-tool-starter/`
6+
7+
Use that folder when creating a new tool under `tools/<tool-id>/`.
8+
9+
## Template Types
10+
11+
- `first-class-tool-starter/`: official first-class tool starter pattern.
12+
- `starter-project-template/`: existing shared project manifest starter.
13+
- `vector-native-arcade/`: existing vector-native arcade project/game starter.
14+
15+
The project/game starter folders are preserved because other repo surfaces still reference them. Do not use them as the starter pattern for new first-class tools.
16+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# First-Class Tool Starter
2+
3+
This folder is the official starter pattern for new first-class tools.
4+
5+
Copy this folder to `tools/<tool-id>/`, then rename the class prefixes, visible title, and tool id references for the new tool.
6+
7+
## Folder Structure
8+
9+
```text
10+
tools/<tool-id>/
11+
index.html
12+
README.md
13+
styles/
14+
toolStarter.css
15+
js/
16+
bootstrap.js
17+
ToolStarterApp.js
18+
controls/
19+
AccordionSection.js
20+
ActionNavControl.js
21+
InspectorControl.js
22+
PreviewPanelControl.js
23+
SourceInputControl.js
24+
StatusLogControl.js
25+
services/
26+
ToolStateSerializer.js
27+
```
28+
29+
## Required Files
30+
31+
- `index.html`: semantic tool shell with no inline `<script>`, no inline `<style>`, and no inline event handlers.
32+
- `styles/*.css`: all tool styles.
33+
- `js/bootstrap.js`: small startup file that creates class instances and wires dependencies.
34+
- `js/ToolStarterApp.js`: app/root coordinator only.
35+
- `js/controls/*.js`: one class per UI control or section.
36+
- `js/services/*.js`: focused non-UI helper classes when needed.
37+
- `README.md`: tool-specific usage, contracts, and validation notes.
38+
39+
## Architecture Rules
40+
41+
- One class per file.
42+
- One control or section per class.
43+
- App/root class coordinates only and must not own DOM logic or business logic.
44+
- Controls own their DOM and their events.
45+
- Controls communicate through injected callbacks or the app coordinator.
46+
- Reusable UI behavior must live in reusable classes such as `AccordionSection`.
47+
- Do not depend on `tools/shared`.
48+
- Do not use inline event handlers such as `onclick`, `onchange`, or `oninput`.
49+
- Do not add hidden defaults or silent fallback data.
50+
51+
## Creating A New Tool
52+
53+
1. Copy `tools/templates/first-class-tool-starter/` to `tools/<tool-id>/`.
54+
2. Rename `ToolStarterApp` and control class names to match the new tool.
55+
3. Update the visible title, subtitle, and `data-tool-id`.
56+
4. Replace starter control labels with the real tool controls.
57+
5. Keep JavaScript external and modular.
58+
6. Keep CSS external.
59+
7. Add first-class tool registry, index, and NAV wiring where applicable.
60+
8. Add targeted Playwright launch coverage for the new tool.
61+
62+
## Playwright Launch Coverage
63+
64+
Every new first-class tool must include Playwright coverage that launches the tool and validates meaningful behavior.
65+
66+
Minimum coverage:
67+
68+
- tool page loads without runtime errors
69+
- primary user action can be triggered or correctly blocked
70+
- required controls render and respond
71+
- accordion sections expand and collapse
72+
- at least one failure state is visible when invalid input is applicable
73+
74+
The required validation command for impacted tool runtime or UI work is:
75+
76+
`npm run test:workspace-v2`
77+
78+
## Review Artifacts
79+
80+
Every PR that creates or changes a first-class tool must produce:
81+
82+
- `docs/dev/reports/codex_review.diff`
83+
- `docs/dev/reports/codex_changed_files.txt`
84+
- a PR-specific report under `docs/dev/reports/`
85+
- a repo-structured delta ZIP under `tmp/`
86+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>First-Class Tool Starter</title>
7+
<link rel="stylesheet" href="./styles/toolStarter.css">
8+
<script type="module" src="./js/bootstrap.js"></script>
9+
</head>
10+
<body>
11+
<header id="shared-theme-header" class="tool-starter__header">
12+
<div>
13+
<h1>First-Class Tool Starter</h1>
14+
<p>Replace this shell with the new tool name and purpose.</p>
15+
</div>
16+
<p class="tool-starter__header-meta">Self-contained starter</p>
17+
</header>
18+
19+
<nav class="tool-starter__menu" aria-label="Tool actions">
20+
<button id="runToolButton" type="button">Run</button>
21+
<button id="resetToolButton" type="button">Reset</button>
22+
<button id="exportToolStateButton" type="button">Export toolState</button>
23+
</nav>
24+
25+
<main class="tool-starter app-shell" data-tool-id="first-class-tool-starter">
26+
<aside class="tool-starter__panel tool-starter__panel--left" aria-label="Tool inputs">
27+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-open="true">
28+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="sourceInputContent">
29+
<span>Input Source</span>
30+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
31+
</button>
32+
<div id="sourceInputContent" class="accordion-v2__content">
33+
<label class="tool-starter__field" for="sourceInput">
34+
<span>Source value</span>
35+
<input id="sourceInput" type="text" autocomplete="off" placeholder="Enter a value to process">
36+
</label>
37+
<p id="sourceValidationMessage" class="tool-starter__hint">Input is required before Run can process.</p>
38+
</div>
39+
</section>
40+
41+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-open="true">
42+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="statusLogContent">
43+
<span>Status</span>
44+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
45+
</button>
46+
<div id="statusLogContent" class="accordion-v2__content">
47+
<button id="clearStatusButton" type="button">Clear</button>
48+
<textarea id="statusLog" readonly rows="10" aria-label="Status log"></textarea>
49+
</div>
50+
</section>
51+
</aside>
52+
53+
<section class="tool-starter__panel tool-starter__panel--center" aria-label="Tool preview">
54+
<section class="accordion-v2 tool-starter__accordion tool-starter__accordion--fill is-open" data-accordion-open="true">
55+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="previewPanelContent">
56+
<span>Preview</span>
57+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
58+
</button>
59+
<div id="previewPanelContent" class="accordion-v2__content">
60+
<div id="previewOutput" class="tool-starter__preview" role="img" aria-label="Tool preview output">
61+
<p>No preview rendered yet.</p>
62+
</div>
63+
</div>
64+
</section>
65+
</section>
66+
67+
<aside class="tool-starter__panel tool-starter__panel--right" aria-label="Tool output">
68+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-open="true">
69+
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="inspectorContent">
70+
<span>Output Summary</span>
71+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
72+
</button>
73+
<div id="inspectorContent" class="accordion-v2__content">
74+
<pre id="inspectorOutput" class="tool-starter__output">{}</pre>
75+
</div>
76+
</section>
77+
</aside>
78+
</main>
79+
</body>
80+
</html>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
export class ToolStarterApp {
2+
constructor({ accordions, actionNav, inspector, preview, serializer, sourceInput, statusLog }) {
3+
this.accordions = accordions;
4+
this.actionNav = actionNav;
5+
this.inspector = inspector;
6+
this.preview = preview;
7+
this.serializer = serializer;
8+
this.sourceInput = sourceInput;
9+
this.statusLog = statusLog;
10+
}
11+
12+
start() {
13+
this.accordions.forEach((accordion) => accordion.mount());
14+
this.actionNav.mount({
15+
onExport: () => this.exportToolState(),
16+
onReset: () => this.reset(),
17+
onRun: () => this.run()
18+
});
19+
this.sourceInput.mount({
20+
onChange: () => this.refreshActions()
21+
});
22+
this.statusLog.mount();
23+
this.preview.clear();
24+
this.inspector.showObject({});
25+
this.refreshActions();
26+
this.statusLog.write("Starter template ready.");
27+
}
28+
29+
run() {
30+
const validation = this.sourceInput.validate();
31+
if (!validation.valid) {
32+
this.preview.clear();
33+
this.statusLog.error(validation.message);
34+
this.refreshActions();
35+
return;
36+
}
37+
38+
const toolState = this.serializer.createToolState({ sourceValue: validation.value });
39+
this.preview.render(toolState.payload);
40+
this.inspector.showObject(toolState);
41+
this.statusLog.write(`Processed source value: ${validation.value}`);
42+
this.refreshActions();
43+
}
44+
45+
reset() {
46+
this.sourceInput.clear();
47+
this.preview.clear();
48+
this.inspector.showObject({});
49+
this.statusLog.write("Starter template reset.");
50+
this.refreshActions();
51+
}
52+
53+
exportToolState() {
54+
const validation = this.sourceInput.validate();
55+
if (!validation.valid) {
56+
this.statusLog.error(validation.message);
57+
this.refreshActions();
58+
return;
59+
}
60+
61+
const toolState = this.serializer.createToolState({ sourceValue: validation.value });
62+
this.inspector.showObject(toolState);
63+
this.statusLog.write("toolState preview written to Output Summary.");
64+
this.refreshActions();
65+
}
66+
67+
refreshActions() {
68+
this.actionNav.setRunEnabled(this.sourceInput.hasValue());
69+
}
70+
}
71+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ToolStarterApp } from "./ToolStarterApp.js";
2+
import { ActionNavControl } from "./controls/ActionNavControl.js";
3+
import { AccordionSection } from "./controls/AccordionSection.js";
4+
import { InspectorControl } from "./controls/InspectorControl.js";
5+
import { PreviewPanelControl } from "./controls/PreviewPanelControl.js";
6+
import { SourceInputControl } from "./controls/SourceInputControl.js";
7+
import { StatusLogControl } from "./controls/StatusLogControl.js";
8+
import { ToolStateSerializer } from "./services/ToolStateSerializer.js";
9+
10+
function requireElement(selector) {
11+
const element = document.querySelector(selector);
12+
if (!element) {
13+
throw new Error(`Missing required starter template element: ${selector}`);
14+
}
15+
return element;
16+
}
17+
18+
window.addEventListener("DOMContentLoaded", () => {
19+
const accordions = Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section));
20+
const sourceInput = new SourceInputControl({
21+
input: requireElement("#sourceInput"),
22+
validationMessage: requireElement("#sourceValidationMessage")
23+
});
24+
const statusLog = new StatusLogControl({
25+
log: requireElement("#statusLog"),
26+
clearButton: requireElement("#clearStatusButton")
27+
});
28+
const app = new ToolStarterApp({
29+
accordions,
30+
actionNav: new ActionNavControl({
31+
runButton: requireElement("#runToolButton"),
32+
resetButton: requireElement("#resetToolButton"),
33+
exportButton: requireElement("#exportToolStateButton")
34+
}),
35+
inspector: new InspectorControl(requireElement("#inspectorOutput")),
36+
preview: new PreviewPanelControl(requireElement("#previewOutput")),
37+
serializer: new ToolStateSerializer("first-class-tool-starter"),
38+
sourceInput,
39+
statusLog
40+
});
41+
42+
app.start();
43+
});
44+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export class AccordionSection {
2+
constructor(section) {
3+
this.section = section;
4+
this.header = section?.querySelector(".accordion-v2__header") || null;
5+
this.content = section?.querySelector(".accordion-v2__content") || null;
6+
}
7+
8+
mount() {
9+
if (!this.section || !this.header || !this.content || this.header.dataset.accordionBound === "true") {
10+
return;
11+
}
12+
13+
this.header.dataset.accordionBound = "true";
14+
this.setOpen(this.section.dataset.accordionOpen !== "false");
15+
this.header.addEventListener("click", () => {
16+
this.setOpen(!this.section.classList.contains("is-open"));
17+
});
18+
}
19+
20+
setOpen(isOpen) {
21+
this.section.classList.toggle("is-open", isOpen);
22+
this.section.dataset.accordionOpen = String(isOpen);
23+
this.header.setAttribute("aria-expanded", String(isOpen));
24+
this.content.hidden = !isOpen;
25+
}
26+
}
27+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class ActionNavControl {
2+
constructor({ runButton, resetButton, exportButton }) {
3+
this.runButton = runButton;
4+
this.resetButton = resetButton;
5+
this.exportButton = exportButton;
6+
}
7+
8+
mount({ onRun, onReset, onExport }) {
9+
this.runButton.addEventListener("click", onRun);
10+
this.resetButton.addEventListener("click", onReset);
11+
this.exportButton.addEventListener("click", onExport);
12+
}
13+
14+
setRunEnabled(isEnabled) {
15+
this.runButton.disabled = !isEnabled;
16+
this.exportButton.disabled = !isEnabled;
17+
}
18+
}
19+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class InspectorControl {
2+
constructor(output) {
3+
this.output = output;
4+
}
5+
6+
showObject(value) {
7+
this.output.textContent = JSON.stringify(value, null, 2);
8+
}
9+
}
10+

0 commit comments

Comments
 (0)