/dial-inject
How a dial physically lives on the user's page: the handles it owns, the four operations that drive it, and the rules that keep measurements honest and teardown total. `dial-inline.md` calls these operations by name; this file is the only place their payloads are defined.
$ npx -y skills add drobins25/craft --agent claude-codeHow it fires
How this command gets triggered: by you, by Claude, or both.
- Fires itselfClaude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/dial-inject
Context preview
What this command does when you run it.
How a dial physically lives on the user's page: the handles it owns, the four operations that drive it, and the rules that keep measurements honest and teardown total. `dial-inline.md` calls these operations by name; this file is the only place their payloads are defined.
Command definition
dial-inject.mdDial Injection Contract
How a dial physically lives on the user's page: the handles it owns, the four operations that drive it, and the rules that keep measurements honest and teardown total. `dial-inline.md` calls these operations by name; this file is the only place their payloads are defined.
The three handles
Everything dial injects hangs off exactly three namespaced handles:
1. **`<style id="craft-dial-style">`** - appended to `document.head`. Holds ALL candidate CSS for the session. 2. **`<div id="craft-dial-panel">`** - appended to `document.body` as its **last child**. The fixed control panel. 3. **`document.documentElement.dataset.craftDial`** - the active position, as a lowercase letter (`"a"`, `"b"`, ...). Set on `<html>`, never on any element inside the app.
Any OTHER real node a candidate injects (an approach position's clickable button, badge, or bar - pseudo-elements can't be interactive) MUST carry the attribute **`data-craft-dial-injected`**. The marker is what makes CLEAR total by construction, regardless of injection technique. An unmarked append is a contract violation - it survives teardown and haunts the page.
**React-safe placement rule:** head, body-last-child, and the `<html>` attribute are all outside any framework root, so a re-render cannot strip them. An **in-tree append is forbidden** - React (and every virtual-DOM framework) reconciles only its own root's children, and anything foreign inside that root is subject to removal on the next render. This placement is why the panel survives.
Candidate CSS shape
Every candidate rule is keyed off the `<html>` attribute, inside the single style element:
html[data-craft-dial="a"] .filter-row { gap: 32px; }
html[data-craft-dial="b"] .filter-row { gap: 24px; }
html[data-craft-dial="c"] .filter-row { gap: 22px; }Toggling a position sets ONE attribute and mutates nothing else. No inline styles on app elements, no class additions, no per-toggle style rewrites - the whole session's candidates coexist in the one stylesheet and the attribute picks the live one.
For **approach** positions that add an element: prefer `::before`/`::after` content under the same `html[data-craft-dial="x"]` key - pseudo-elements are removed with the stylesheet for free. When the candidate needs a real, interactive node (a clickable button), create it with `data-craft-dial-injected` set, so CLEAR's bulk selector removes it. Never an unmarked append.
The four operations
Each is one `evaluate_script` payload. `dial-inline.md` refers to them by name.
MEASURE
Read the surface before and during the session. Returns a small JSON-ish object - never a full accessibility tree:
(() => {
const els = [...document.querySelectorAll('<selector>')]
.filter(el => el.getClientRects().length > 0 &&
getComputedStyle(el).visibility !== 'hidden');
if (!els.length) return { found: 0 };
const el = els[0];
const cs = getComputedStyle(el);
const r = el.getBoundingClientRect();
return {
found: els.length,
selector: '<selector>',
value: cs.getPropertyValue('<property>'),
rect: { x: r.x, y: r.y, w: r.width, h: r.height }
};
})()**Visibility rule (non-negotiable):** every measured or targeted element must pass `el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden'`. Responsive pages carry BOTH their desktop and mobile faces in the DOM; the hidden face has zero rects and lies about the layout. A bare `querySelector` with no visibility filter is the default mistake - it reports numbers from an element nobody can see.
INJECT
One payload creates all three handles. Candidate CSS and panel markup are built by the flow; the placement is fixed:
(() => {
const style = document.createElement('style');
style.id = 'craft-dial-style';
style.textContent = `<candidate CSS rules>`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'craft-dial-panel';
panel.innerHTML = `<panel markup>`;
document.body.appendChild(panel);
document.documentElement.dataset.craftDial = 'a';
})()Re-injection (a new position mid-session, a refresh recovery) REPLACES the style element's `textContent` and the panel's `innerHTML` through the same ids - it never appends a second style or panel. Positions are never removed once injected; the letter set only grows.
TOGGLE
document.documentElement.dataset.craftDial = '<letter>';
That is the entire operation. The panel's click handlers do the same thing from inside the page; a chat-driven toggle does it via `evaluate_script`.
**Re-measure rule (non-negotiable):** after every toggle, any number reported to the user comes from a fresh MEASURE - a live `getBoundingClientRect()` / `getComputedStyle` read - never from the CSS value that was injected. An injected value that lost a specificity fight, hit a `!important`, or missed its selector reports as "applied" unless it is read back. The readout's whole job is being the receipt that the CSS actually landed.
CLEAR
The canonical clear, verbatim. This exact snippet - same ids, same order - is what every flow runs; a paraphrase breaks the coverage grep that keeps the call sites aligned:
document.getElementById('craft-dial-style')?.remove();
document.getElementById('craft-dial-panel')?.remove();
document.querySelectorAll('[data-craft-dial-injected]').forEach(el => el.remove());
delete document.documentElement.dataset.craftDial;CLEAR is **idempotent on a page that was never dialed**: optional-chained removal no-ops on absent nodes, the bulk selector matches nothing, and `delete` on an absent dataset key is a silent no-op. Run it unconditionally - before a dial starts (a stale session may have left residue) and on every exit path including abandonment.
The panel
**Visual register: flat, grey, unmistakably scaffolding.** Dev chrome, never design - anything that looks like design risks bein
Read more
Dial Injection Contract
How a dial physically lives on the user's page: the handles it owns, the four operations that drive it, and the rules that keep measurements honest and teardown total. `dial-inline.md` calls these operations by name; this file is the only place their payloads are defined.
The three handles
Everything dial injects hangs off exactly three namespaced handles:
1. **`<style id="craft-dial-style">`** - appended to `document.head`. Holds ALL candidate CSS for the session. 2. **`<div id="craft-dial-panel">`** - appended to `document.body` as its **last child**. The fixed control panel. 3. **`document.documentElement.dataset.craftDial`** - the active position, as a lowercase letter (`"a"`, `"b"`, ...). Set on `<html>`, never on any element inside the app.
Any OTHER real node a candidate injects (an approach position's clickable button, badge, or bar - pseudo-elements can't be interactive) MUST carry the attribute **`data-craft-dial-injected`**. The marker is what makes CLEAR total by construction, regardless of injection technique. An unmarked append is a contract violation - it survives teardown and haunts the page.
**React-safe placement rule:** head, body-last-child, and the `<html>` attribute are all outside any framework root, so a re-render cannot strip them. An **in-tree append is forbidden** - React (and every virtual-DOM framework) reconciles only its own root's children, and anything foreign inside that root is subject to removal on the next render. This placement is why the panel survives.
Candidate CSS shape
Every candidate rule is keyed off the `<html>` attribute, inside the single style element:
html[data-craft-dial="a"] .filter-row { gap: 32px; }
html[data-craft-dial="b"] .filter-row { gap: 24px; }
html[data-craft-dial="c"] .filter-row { gap: 22px; }Toggling a position sets ONE attribute and mutates nothing else. No inline styles on app elements, no class additions, no per-toggle style rewrites - the whole session's candidates coexist in the one stylesheet and the attribute picks the live one.
For **approach** positions that add an element: prefer `::before`/`::after` content under the same `html[data-craft-dial="x"]` key - pseudo-elements are removed with the stylesheet for free. When the candidate needs a real, interactive node (a clickable button), create it with `data-craft-dial-injected` set, so CLEAR's bulk selector removes it. Never an unmarked append.
The four operations
Each is one `evaluate_script` payload. `dial-inline.md` refers to them by name.
MEASURE
Read the surface before and during the session. Returns a small JSON-ish object - never a full accessibility tree:
(() => {
const els = [...document.querySelectorAll('<selector>')]
.filter(el => el.getClientRects().length > 0 &&
getComputedStyle(el).visibility !== 'hidden');
if (!els.length) return { found: 0 };
const el = els[0];
const cs = getComputedStyle(el);
const r = el.getBoundingClientRect();
return {
found: els.length,
selector: '<selector>',
value: cs.getPropertyValue('<property>'),
rect: { x: r.x, y: r.y, w: r.width, h: r.height }
};
})()**Visibility rule (non-negotiable):** every measured or targeted element must pass `el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden'`. Responsive pages carry BOTH their desktop and mobile faces in the DOM; the hidden face has zero rects and lies about the layout. A bare `querySelector` with no visibility filter is the default mistake - it reports numbers from an element nobody can see.
INJECT
One payload creates all three handles. Candidate CSS and panel markup are built by the flow; the placement is fixed:
(() => {
const style = document.createElement('style');
style.id = 'craft-dial-style';
style.textContent = `<candidate CSS rules>`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'craft-dial-panel';
panel.innerHTML = `<panel markup>`;
document.body.appendChild(panel);
document.documentElement.dataset.craftDial = 'a';
})()Re-injection (a new position mid-session, a refresh recovery) REPLACES the style element's `textContent` and the panel's `innerHTML` through the same ids - it never appends a second style or panel. Positions are never removed once injected; the letter set only grows.
TOGGLE
document.documentElement.dataset.craftDial = '<letter>';
That is the entire operation. The panel's click handlers do the same thing from inside the page; a chat-driven toggle does it via `evaluate_script`.
**Re-measure rule (non-negotiable):** after every toggle, any number reported to the user comes from a fresh MEASURE - a live `getBoundingClientRect()` / `getComputedStyle` read - never from the CSS value that was injected. An injected value that lost a specificity fight, hit a `!important`, or missed its selector reports as "applied" unless it is read back. The readout's whole job is being the receipt that the CSS actually landed.
CLEAR
The canonical clear, verbatim. This exact snippet - same ids, same order - is what every flow runs; a paraphrase breaks the coverage grep that keeps the call sites aligned:
document.getElementById('craft-dial-style')?.remove();
document.getElementById('craft-dial-panel')?.remove();
document.querySelectorAll('[data-craft-dial-injected]').forEach(el => el.remove());
delete document.documentElement.dataset.craftDial;CLEAR is **idempotent on a page that was never dialed**: optional-chained removal no-ops on absent nodes, the bulk selector matches nothing, and `delete` on an absent dataset key is a silent no-op. Run it unconditionally - before a dial starts (a stale session may have left residue) and on every exit path including abandonment.
The panel
**Visual register: flat, grey, unmistakably scaffolding.** Dev chrome, never design - anything that looks like design risks bein
Stop Vibing. Start Crafting. Claude Code plugin: guided + controlled development orchestration harness with built-in workflow + state management, for designing + building durable, production-ready software through the entire product lifecycle - new projects
Repo: drobins25/craft
Other commands on craft.
- /craft-analyze
Post-cycle analysis — QA, UX, Creative, and Style audits using MCP browser tools.
Open command - /craft-ask
Consult a craft agent. Routes your question to the best mind in the workshop - not a menu, a recommendation.
Open command - /craft-become
Agent crystallization command. Studies a tool, role, or person and produces a portable 9-section agent that inhabits the domain - with beliefs, scar tissue, and instincts.
Open command - /craft-cycle-assign
Move a story from backlog to a cycle.
Open command - /craft-cycle-complete
Complete a cycle. Triggers reflection if pending learnings, then archives.
Open command - /craft-cycle-design
Design a cycle — create new cycles with planned stories, detail existing planning cycles, or quick-sketch a roadmap. Detects planning docs in .craft/planning/ and sources the cycle from them when relevant.
Open command

