Skip to content

DOJ-4798: add workbook-generate (per-course interactive HTML workbook)#27

Merged
dbejarano820 merged 5 commits into
mainfrom
daniel/doj-4798-interactive-course-workbook
Jun 2, 2026
Merged

DOJ-4798: add workbook-generate (per-course interactive HTML workbook)#27
dbejarano820 merged 5 commits into
mainfrom
daniel/doj-4798-interactive-course-workbook

Conversation

@dbejarano820
Copy link
Copy Markdown
Contributor

Summary

Adds the IDTK workbook-generate surface — a voice-neutral command + skill that turns a course's text classes into one standalone, accessible, interactive HTML workbook (Rise/Typeform-style continuous flow), held to the editorial quality bar of interactive-HTML explainers like html-effectiveness.

Student-facing sibling of course-visualize (per-course HTML). Distinct from slides-generate (video briefs). Per-course by default; --scope module narrows.

Files (3 new)

  • commands/workbook-generate.md — spec resolution (sibling repo → remote → graceful-degrade), per-course discovery + ordering of text-*.md, content→component mapping, overlay invocation, ${CLAUDE_PLUGIN_ROOT} defensive note.
  • skills/workbook-generate/SKILL.md — the IDT-owned, voice-neutral component catalog + content-shape→component mapping rules + accessibility contract + consumer-spec consumption (the consumer YAML themes/tunes; it never defines the library).
  • assets/templates/workbook/workbook-base.html — self-contained template: design-system CSS layer (type/spacing/radius/shadow scales, 1.5px borders, panels + elevation, mono eyebrows, editorial heading weight + negative tracking, ~680px measure, 300ms SVG transitions), vanilla-JS engine, full component kit — progressive disclosure (accordion/tabs/reveal), inline SVG (flow/comparison/chart/figure), annotated code, multiple-choice + free-text, comparison table, term→glossary micro-interaction, course-nav TOC + progress.

Constraints honored

  • Standalone: no CDN runtime deps (only the allowed Google Fonts <link> + system-ui fallback).
  • In-memory state only — no localStorage/postMessage/iframe (persistence deferred to V2).
  • Voice-neutral IDT base — zero hardcoded Dojo palette/step-patterns; brand via the consumer's instruction-bundle-spec.yaml.
  • Accessibility: keyboard nav, prefers-reduced-motion, ARIA landmarks/progressbar, focus-visible rings, AA contrast, text+icon quiz feedback.

Test plan

  • python3 scripts/ci/check_frontmatter.py && check_agent_references.py && check_json_schemas.py → all green
  • Rendered in-browser: hero/eyebrows/callout/accordion/tabs/dark code block — editorial polish confirmed
  • Interactions exercised: accordion, tabs, quiz (auto-highlight + feedback), reveal, term→glossary highlight, code copy, progress — zero console errors
  • Design tokens verified applied (1.5px border, heading weight 600, line-height 1.65, 0.3s SVG transitions, mono table headers, good/bad cells)

Linear

Part of DOJ-4798 (spike result of DOJ-4783). Paired with dojo-academy PR (workbooks spec preferences). Works today via graceful defaults even before that lands.

Created by Claude Code on behalf of @daniel

🤖 Generated with Claude Code

…mplate

Voice-neutral IDTK surface that turns a course's text classes into one
standalone, accessible, interactive HTML workbook (Rise/Typeform-style),
held to the editorial quality bar of interactive-HTML explainers.

- commands/workbook-generate.md — spec resolution (sibling/remote/graceful-
  degrade), per-course discovery + ordering, content->component mapping,
  overlay invocation, ${CLAUDE_PLUGIN_ROOT} defensive note
- skills/workbook-generate/SKILL.md — IDT-owned component catalog + mapping
  rules + accessibility contract + consumer-spec consumption rules
- assets/templates/workbook/workbook-base.html — self-contained template:
  design-system CSS layer (type/spacing/radius/shadow scales, 1.5px borders,
  panels+elevation, mono eyebrows, editorial headings), vanilla-JS engine,
  full component kit (progressive disclosure, inline SVG, annotated code,
  quiz/free-text, comparison table, term->glossary)

State is in-memory only (persistence deferred to V2). No CDN runtime deps.
The consumer's instruction-bundle-spec.yaml themes + optionally tunes the
catalog; the library itself stays in IDT base, voice-neutral. CI lint green.

Created by Claude Code on behalf of @daniel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dbejarano820
Copy link
Copy Markdown
Contributor Author

@dojo-code-reviewer review

Copy link
Copy Markdown

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Approved — 0 blockers, 1 P2, 3 P3. Confidence: 3/5.

🟡 P2 — Major

  • assets/templates/workbook/workbook-base.html:512 — 🟡 P2 (major) — Global keydown event listeners for Enter and arrow keys will capture events even when focus is inside active controls like buttons, links, or tabs. This triggers accidental slide navigation when a student presses Enter to expand an accordion, activate a tab, or choose a quiz option. Restricting keydown listener execution to exclude interactions with focused active components prevents overlapping navigation actions.
      if (e.target.closest("textarea, input, button, select, a, [role='tab']")) return;

🔵 P3 — Minor

  • assets/templates/workbook/workbook-base.html:450 — 🔵 P3 (minor) — Generating a workbook with zero step elements causes a runtime error when trying to access steps[idx] in show(0) or when dividing by zero in progress calculations. Adding an early-return guard when steps is empty prevents JS execution errors.
  if (steps.length === 0) return;
  • assets/templates/workbook/workbook-base.html:548 — 🔵 P3 (minor) — Double-clicking the code copy button before the 1200ms timeout resets its text captures 'Copied' as the original label. When the timer triggers, this leaves the button perpetually stuck showing 'Copied'. Ignoring click events when the button is already in the copied state eliminates this race condition.
    btn.addEventListener("click", function () {
      if (btn.textContent === "Copied") return;
  • assets/templates/workbook/workbook-base.html:559 — 🔵 P3 (minor) — Selecting glossary definitions with unescaped attribute selectors fails with a DOMException if a glossary term contains special characters like double quotes or parentheses. Iterating through dt elements and validating their data-term attribute prevents unexpected runtime CSS selector syntax errors.
  function glossaryEntry(term) {
    var dts = document.querySelectorAll(".wb-glossary dt[data-term]");
    for (var i = 0; i < dts.length; i++) {
      if (dts[i].getAttribute("data-term") === term) return dts[i];
    }
    return null;
  }

Total findings: 1 compliance, 3 business context (4 total)


ℹ️ Inline anchoring failed for the comments below; they are listed here instead.

  • assets/templates/workbook/workbook-base.html:450 — 🔵 P3 (minor) — Generating a workbook with zero step elements causes a runtime error when trying to access steps[idx] in show(0) or when dividing by zero in progress calculations. Adding an early-return guard when steps is empty prevents JS execution errors.
  if (steps.length === 0) return;
  • assets/templates/workbook/workbook-base.html:512 — 🟡 P2 (major) — Global keydown event listeners for Enter and arrow keys will capture events even when focus is inside active controls like buttons, links, or tabs. This triggers accidental slide navigation when a student presses Enter to expand an accordion, activate a tab, or choose a quiz option. Restricting keydown listener execution to exclude interactions with focused active components prevents overlapping navigation actions.
      if (e.target.closest("textarea, input, button, select, a, [role='tab']")) return;
  • assets/templates/workbook/workbook-base.html:548 — 🔵 P3 (minor) — Double-clicking the code copy button before the 1200ms timeout resets its text captures 'Copied' as the original label. When the timer triggers, this leaves the button perpetually stuck showing 'Copied'. Ignoring click events when the button is already in the copied state eliminates this race condition.
    btn.addEventListener("click", function () {
      if (btn.textContent === "Copied") return;
  • assets/templates/workbook/workbook-base.html:559 — 🔵 P3 (minor) — Selecting glossary definitions with unescaped attribute selectors fails with a DOMException if a glossary term contains special characters like double quotes or parentheses. Iterating through dt elements and validating their data-term attribute prevents unexpected runtime CSS selector syntax errors.
  function glossaryEntry(term) {
    var dts = document.querySelectorAll(".wb-glossary dt[data-term]");
    for (var i = 0; i < dts.length; i++) {
      if (dts[i].getAttribute("data-term") === term) return dts[i];
    }
    return null;
  }

Folds the strongest, reliability-preserving patterns from the
html-effectiveness deep-dive into the workbook base + skill:

- clipboard copy now has a file:// fallback (execCommand + off-screen
  textarea) — fixes a silent no-op when the workbook opens as a local file
- inline code diff variant (struck removed lines + added lines)
- bar chart with precomputed coords + recolored salient datum (.is-peak)
- parametric "knob": slider -> CSS var -> live preview — the reliable way to
  deliver "felt" interactivity without bespoke per-concept JS
- skill: documents the new components, the semantic color language
  (accent=current / good=success / bad=error), the chart-highlight + diff
  mapping heuristics; promotes the knob to first-class, leaves
  live-simulation/state-visualizer as the fragile stretch tier

Verified in preview: all new components render + work, zero console errors.
CI lint green.

Created by Claude Code on behalf of @daniel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Approved — 0 blockers, 1 P2, 1 P3. Confidence: 3/5.

🟡 P2 — Major

  • assets/templates/workbook/workbook-base.html:499 — 🟡 P2 (major) — In stepped mode, the global document keydown listener intercepts all "Enter" key presses to advance the workbook step unless the event target matches "textarea, input". Because the interactive workbook components (accordions, tabs, quiz options, code copy buttons, etc.) are built using <button> or <a> elements, pressing "Enter" to activate them will trigger step navigation as well. This severely violates keyboard accessibility guidelines.

To fix this, update the focus check to exclude all standard interactive elements from triggering page navigation on Enter.

      if (e.target.matches("button, a, textarea, input, select, option, [role='button'], [role='tab']")) return;

🔵 P3 — Minor

  • assets/templates/workbook/workbook-base.html:543 — 🔵 P3 (minor) — Complete the roving tabindex implementation for the tabbed interface. Currently, all tabs in the list are standard focusable <button> elements without explicit tabindex attributes. This means keyboard users must Tab through every single tab in the list sequentially, which violates the ARIA Tab Panel design pattern.

Updating unselected tabs to tabindex="-1" and the active tab to tabindex="0" ensures the tablist is navigated via arrow keys as intended and acts as a single tab stop.


Total findings: 1 compliance, 1 business context (2 total)


ℹ️ The findings below could not be anchored to a diff line (line not part of any hunk); they are listed here instead.

  • assets/templates/workbook/workbook-base.html:499 — 🟡 P2 (major) — In stepped mode, the global document keydown listener intercepts all "Enter" key presses to advance the workbook step unless the event target matches "textarea, input". Because the interactive workbook components (accordions, tabs, quiz options, code copy buttons, etc.) are built using <button> or <a> elements, pressing "Enter" to activate them will trigger step navigation as well. This severely violates keyboard accessibility guidelines.

To fix this, update the focus check to exclude all standard interactive elements from triggering page navigation on Enter.

      if (e.target.matches("button, a, textarea, input, select, option, [role='button'], [role='tab']")) return;

});
});

// ---- Tabs (roving) ----
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 P3 (minor) — Complete the roving tabindex implementation for the tabbed interface. Currently, all tabs in the list are standard focusable <button> elements without explicit tabindex attributes. This means keyboard users must Tab through every single tab in the list sequentially, which violates the ARIA Tab Panel design pattern.

Updating unselected tabs to tabindex="-1" and the active tab to tabindex="0" ensures the tablist is navigated via arrow keys as intended and acts as a single tab stop.

…+ export

Per the html-effectiveness thesis, interactivity here serves display + felt
understanding + closing the loop — never capturing answers the artifact can't
persist or act on. So:

- remove the multiple-choice quiz and free-text input (dead ends with no
  persistence and no export)
- add predict-and-reveal: a "think first, then reveal the model answer"
  self-check (retrieval + immediate feedback, no capture, no grading)
- add export / copy-as-prompt: a first-class loop-closer that copies a
  ready-made prompt to the clipboard (file:// fallback) so the learner takes
  work back to Claude or a mentor
- consolidate the clipboard helper; document "what interactivity is for", the
  new components, and the capture-to-nowhere anti-pattern in the skill

Verified in preview: predict-and-reveal toggles, export copies + flashes, no
quiz/free-text remain, zero console errors. CI lint green.

Created by Claude Code on behalf of @daniel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes Requested

Changes requested — 2 blockers, 1 P2, 1 P3. Confidence: 1/5.

🔴 P1 — Blockers

  • assets/templates/workbook/workbook-base.html:516 — 🔴 P1 (blocker) — Intercepting keydowns globally on document in stepped mode intercepts standard keyboard controls for buttons, links, and tabs. For example, pressing "Enter" on the focused "Next" button triggers both the keydown and the click events, causing the workbook to skip two steps at a time. It also prevents accordion triggers or "Reveal" buttons from being activated by the keyboard, navigating the page instead. Exclude active interactive elements from the global listener.
      if (e.target.closest("button, a, input, textarea, [role='tab'], [role='button']")) return;
  • assets/templates/workbook/workbook-base.html:575 — 🔴 P1 (blocker) — Reading .innerText on a hidden DOM element (the pre element has the hidden attribute, which uses display: none) returns an empty string "" in modern browsers. This causes the "Copy as prompt" button to copy nothing to the clipboard. Use .textContent instead to ensure the hidden prompt text is copied correctly.
      if (payload) copyToClipboard(payload.textContent, btn, "Copied ✓");

🟡 P2 — Major

  • assets/templates/workbook/workbook-base.html:564 — 🟡 P2 (major) — Copying the raw innerText of a code diff block copies both deleted (.diff-del) and added (.diff-add) lines, resulting in malformed/unusable code in the clipboard. Clean the diff before copying by removing elements with the .diff-del class from a cloned node to copy only the final "after" state of the code.
  document.querySelectorAll(".wb-code__copy").forEach(function (btn) {
    btn.addEventListener("click", function () {
      var code = btn.parentElement.querySelector("code");
      if (code) {
        var text = code.innerText;
        if (code.querySelector(".diff-del")) {
          var clone = code.cloneNode(true);
          clone.querySelectorAll(".diff-del").forEach(function (el) { el.remove(); });
          text = clone.innerText;
        }
        copyToClipboard(text, btn);
      }
    });
  });

🔵 P3 — Minor

  • assets/templates/workbook/workbook-base.html:506 — 🔵 P3 (minor) — Calculating progress by dividing by steps.length yields NaN or Infinity if the workbook contains zero steps. Avoid potential rendering errors or invalid inline style values by checking steps.length defensively.
    setProgress(steps.length > 0 ? ((idx + 1) / steps.length) * 100 : 0);

Total findings: 2 security, 1 compliance, 1 business context (4 total)


ℹ️ Inline anchoring failed for the comments below; they are listed here instead.

  • assets/templates/workbook/workbook-base.html:516 — 🔴 P1 (blocker) — Intercepting keydowns globally on document in stepped mode intercepts standard keyboard controls for buttons, links, and tabs. For example, pressing "Enter" on the focused "Next" button triggers both the keydown and the click events, causing the workbook to skip two steps at a time. It also prevents accordion triggers or "Reveal" buttons from being activated by the keyboard, navigating the page instead. Exclude active interactive elements from the global listener.
      if (e.target.closest("button, a, input, textarea, [role='tab'], [role='button']")) return;
  • assets/templates/workbook/workbook-base.html:575 — 🔴 P1 (blocker) — Reading .innerText on a hidden DOM element (the pre element has the hidden attribute, which uses display: none) returns an empty string "" in modern browsers. This causes the "Copy as prompt" button to copy nothing to the clipboard. Use .textContent instead to ensure the hidden prompt text is copied correctly.
      if (payload) copyToClipboard(payload.textContent, btn, "Copied ✓");
  • assets/templates/workbook/workbook-base.html:564 — 🟡 P2 (major) — Copying the raw innerText of a code diff block copies both deleted (.diff-del) and added (.diff-add) lines, resulting in malformed/unusable code in the clipboard. Clean the diff before copying by removing elements with the .diff-del class from a cloned node to copy only the final "after" state of the code.
  document.querySelectorAll(".wb-code__copy").forEach(function (btn) {
    btn.addEventListener("click", function () {
      var code = btn.parentElement.querySelector("code");
      if (code) {
        var text = code.innerText;
        if (code.querySelector(".diff-del")) {
          var clone = code.cloneNode(true);
          clone.querySelectorAll(".diff-del").forEach(function (el) { el.remove(); });
          text = clone.innerText;
        }
        copyToClipboard(text, btn);
      }
    });
  });
  • assets/templates/workbook/workbook-base.html:506 — 🔵 P3 (minor) — Calculating progress by dividing by steps.length yields NaN or Infinity if the workbook contains zero steps. Avoid potential rendering errors or invalid inline style values by checking steps.length defensively.
    setProgress(steps.length > 0 ? ((idx + 1) / steps.length) * 100 : 0);

…ytelling

Reworks the workbook flow + navigation from design review:

- default flow is now `doc` — a navigable document CHUNKED BY MODULE ("lesson"),
  one module shown at a time, switched from the TOC (the Rise lesson model).
  scroll-snap dropped; plain smooth scroll within a lesson. `stepped` mode kept
  for screen-recording.
- navigation: sticky sidebar on desktop; on mobile, a slim sticky bar + a
  collapsible "Contents" menu (was a 441px wall of links pushing content down).
- light-touch storytelling: an arc indicator ("Module N of M") + a forward-hook
  Next control (action line + an OPTIONAL teaser line that is a voice slot —
  empty in the base, filled by the consumer's voice overlay).
- fluid hero type (clamp) + overflow-wrap on headings for small screens.
- command + skill updated for the doc/stepped flow, mobile nav, arc + hook slots.

Verified in preview: chunk switching, mobile collapsible menu, forward-hook +
arc render, no horizontal overflow, zero console errors. CI lint green.

Created by Claude Code on behalf of @daniel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes Requested

Changes requested — 1 blocker, 1 P2, 2 P3. Confidence: 1/5.

🔴 P1 — Blockers

  • assets/templates/workbook/workbook-base.html:672 — 🔴 P1 (blocker) — Calling innerText on a hidden element (the payload pre-tag has the hidden attribute) will return an empty string ("") in all major browsers because innerText is aware of CSS layout and returns empty for hidden nodes. This will result in the user copying an empty string to their clipboard when clicking "Copy as prompt". Use textContent instead, which is not style-aware and successfully retrieves the hidden payload.
      if (payload) copyToClipboard(payload.textContent, btn, "Copied ✓");

🟡 P2 — Major

  • assets/templates/workbook/workbook-base.html:493 — 🟡 P2 (major) — The global keydown listener in stepped mode only ignores textarea and input targets. If a user presses 'Enter' or 'ArrowLeft'/'ArrowRight' while focused on other interactive elements (such as accordions, tabs, buttons, or links), the keydown event will bubble up to the document and trigger step navigation. This causes extremely disruptive behavior during step-level interactions. Exclude all interactive elements using closest on the target.
      if (e.target.closest("textarea, input, button, select, a, [role='tab'], [role='button']")) return;

🔵 P3 — Minor

  • assets/templates/workbook/workbook-base.html:32 — 🔵 P3 (minor) — --wb-accent-soft is hardcoded to a dark gray RGB value (rgba(43,43,43,0.08)). If a consumer specifies a custom brand color for --wb-accent, the soft background will remain gray instead of adapting to the brand palette. Utilizing CSS color-mix allows this soft variant to dynamically derive from the primary accent color across all themes.
  --wb-accent-soft: color-mix(in srgb, var(--wb-accent) 8%, transparent);
  • assets/templates/workbook/workbook-base.html:518 — 🔵 P3 (minor) — Performing document.querySelector directly on a.getAttribute("href") will throw a DOMException and crash the script if the link is external or an invalid selector (e.g. not starting with #). Ensure we safely guard the selection by verifying that the href is an internal hash link, and retrieve it via document.getElementById to ensure robustness.
      a.addEventListener("click", function (e) {
        var href = a.getAttribute("href");
        if (!href || href.charAt(0) !== '#') return;
        var target = document.getElementById(href.slice(1));
        if (!target) return;

Total findings: 1 security, 1 compliance, 2 business context (4 total)


ℹ️ Inline anchoring failed for the comments below; they are listed here instead.

  • assets/templates/workbook/workbook-base.html:672 — 🔴 P1 (blocker) — Calling innerText on a hidden element (the payload pre-tag has the hidden attribute) will return an empty string ("") in all major browsers because innerText is aware of CSS layout and returns empty for hidden nodes. This will result in the user copying an empty string to their clipboard when clicking "Copy as prompt". Use textContent instead, which is not style-aware and successfully retrieves the hidden payload.
      if (payload) copyToClipboard(payload.textContent, btn, "Copied ✓");
  • assets/templates/workbook/workbook-base.html:493 — 🟡 P2 (major) — The global keydown listener in stepped mode only ignores textarea and input targets. If a user presses 'Enter' or 'ArrowLeft'/'ArrowRight' while focused on other interactive elements (such as accordions, tabs, buttons, or links), the keydown event will bubble up to the document and trigger step navigation. This causes extremely disruptive behavior during step-level interactions. Exclude all interactive elements using closest on the target.
      if (e.target.closest("textarea, input, button, select, a, [role='tab'], [role='button']")) return;
  • assets/templates/workbook/workbook-base.html:32 — 🔵 P3 (minor) — --wb-accent-soft is hardcoded to a dark gray RGB value (rgba(43,43,43,0.08)). If a consumer specifies a custom brand color for --wb-accent, the soft background will remain gray instead of adapting to the brand palette. Utilizing CSS color-mix allows this soft variant to dynamically derive from the primary accent color across all themes.
  --wb-accent-soft: color-mix(in srgb, var(--wb-accent) 8%, transparent);
  • assets/templates/workbook/workbook-base.html:518 — 🔵 P3 (minor) — Performing document.querySelector directly on a.getAttribute("href") will throw a DOMException and crash the script if the link is external or an invalid selector (e.g. not starting with #). Ensure we safely guard the selection by verifying that the href is an internal hash link, and retrieve it via document.getElementById to ensure robustness.
      a.addEventListener("click", function (e) {
        var href = a.getAttribute("href");
        if (!href || href.charAt(0) !== '#') return;
        var target = document.getElementById(href.slice(1));
        if (!target) return;

…bustness

Greptile first-pass findings on PR #27:
- P1 (blocker): "Copy as prompt" copied an empty string — innerText returns ""
  for the hidden payload <pre>. Use textContent (style-agnostic, portable).
- P2 (major): stepped-mode global keydown hijacked Enter/arrows while focus was
  on a control (double-advance; blocked accordion/tab/reveal activation). Guard
  with closest() over interactive elements so they activate natively.
- P3: --wb-accent-soft was hardcoded gray; derive from --wb-accent via color-mix
  so the soft tint tracks the consumer's brand.
- P3: TOC click did querySelector(href) (throws on non-hash/external href);
  guard to internal hash + getElementById.

Verified in preview: export copies the non-empty payload, color-mix resolves,
TOC chunk-switching intact, zero console errors. CI lint green.

Created by Claude Code on behalf of @daniel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dbejarano820 dbejarano820 merged commit b6023cf into main Jun 2, 2026
1 check passed
@dbejarano820 dbejarano820 deleted the daniel/doj-4798-interactive-course-workbook branch June 2, 2026 13:42
Copy link
Copy Markdown

@dojo-code-reviewer dojo-code-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved

Approved — 0 blockers, 2 P2, 1 P3. Confidence: 3/5.

🟡 P2 — Major

  • assets/templates/workbook/workbook-base.html:483 — 🟡 P2 (major) — The table of contents links are completely non-functional in stepped mode because the event listener to prevent default anchor behavior and update the active step is only registered in the doc mode branch. Clicking a link in stepped mode will trigger the browser's default hash navigation, which fails to scroll or focus because the target step has display: none set.

Registering a click handler for navLinks inside the stepped mode block that maps the clicked module or step to the correct index and calls show(idx) will make navigation work seamlessly in both modes.

  • assets/templates/workbook/workbook-base.html:629 — 🟡 P2 (major) — The tabs component lacks full keyboard roving tabindex support and automatic activation when navigating via arrow keys. Right now, keyboard users must Tab through every individual tab button since tabindex is not managed, and arrow key focus does not trigger panel selection or update aria-selected status.

Updating tabindex dynamically (active tab is 0, inactive tabs are -1) and automatically selecting the focused tab during arrow key navigation satisfies standard WAI-ARIA guidelines and ensures a high-quality, accessible experience.

  // ---- Tabs (roving & automatic activation) ----
  document.querySelectorAll(".wb-tabs").forEach(function (group) {
    var tabs = Array.prototype.slice.call(group.querySelectorAll('[role="tab"]'));
    function select(tab) {
      tabs.forEach(function (t) {
        var sel = t === tab;
        t.setAttribute("aria-selected", String(sel));
        t.setAttribute("tabindex", sel ? "0" : "-1");
        var panel = document.getElementById(t.getAttribute("aria-controls"));
        if (panel) panel.hidden = !sel;
      });
    }
    tabs.forEach(function (t, i) {
      t.setAttribute("tabindex", t.getAttribute("aria-selected") === "true" ? "0" : "-1");
      t.addEventListener("click", function () { select(t); });
      t.addEventListener("keydown", function (e) {
        var nextTab;
        if (e.key === "ArrowRight") { nextTab = tabs[(i + 1) % tabs.length]; }
        else if (e.key === "ArrowLeft") { nextTab = tabs[(i - 1 + tabs.length) % tabs.length]; }
        if (nextTab) {
          nextTab.focus();
          select(nextTab);
        }
      });
    });
  });

🔵 P3 — Minor

  • assets/templates/workbook/workbook-base.html:253 — 🔵 P3 (minor) — Inline SVGs sharing the duplicate marker ID wb-arrow will conflict when multiple flow diagrams are generated on the same page. The browser will only use the first defined marker, which can cause rendering bugs if custom colors or styles are applied in later diagrams.

Incorporating a unique suffix (e.g., wb-arrow-{{id}}) dynamically during generation inside the workbook-generate command will prevent ID clashing.


Total findings: 2 compliance, 1 business context (3 total)


ℹ️ The findings below could not be anchored to a diff line (line not part of any hunk); they are listed here instead.

  • assets/templates/workbook/workbook-base.html:629 — 🟡 P2 (major) — The tabs component lacks full keyboard roving tabindex support and automatic activation when navigating via arrow keys. Right now, keyboard users must Tab through every individual tab button since tabindex is not managed, and arrow key focus does not trigger panel selection or update aria-selected status.

Updating tabindex dynamically (active tab is 0, inactive tabs are -1) and automatically selecting the focused tab during arrow key navigation satisfies standard WAI-ARIA guidelines and ensures a high-quality, accessible experience.

  // ---- Tabs (roving & automatic activation) ----
  document.querySelectorAll(".wb-tabs").forEach(function (group) {
    var tabs = Array.prototype.slice.call(group.querySelectorAll('[role="tab"]'));
    function select(tab) {
      tabs.forEach(function (t) {
        var sel = t === tab;
        t.setAttribute("aria-selected", String(sel));
        t.setAttribute("tabindex", sel ? "0" : "-1");
        var panel = document.getElementById(t.getAttribute("aria-controls"));
        if (panel) panel.hidden = !sel;
      });
    }
    tabs.forEach(function (t, i) {
      t.setAttribute("tabindex", t.getAttribute("aria-selected") === "true" ? "0" : "-1");
      t.addEventListener("click", function () { select(t); });
      t.addEventListener("keydown", function (e) {
        var nextTab;
        if (e.key === "ArrowRight") { nextTab = tabs[(i + 1) % tabs.length]; }
        else if (e.key === "ArrowLeft") { nextTab = tabs[(i - 1 + tabs.length) % tabs.length]; }
        if (nextTab) {
          nextTab.focus();
          select(nextTab);
        }
      });
    });
  });

<button class="wb-btn" id="wb-next" type="button">Next &rarr;</button>
</div>
</div>
</main>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P2 (major) — The table of contents links are completely non-functional in stepped mode because the event listener to prevent default anchor behavior and update the active step is only registered in the doc mode branch. Clicking a link in stepped mode will trigger the browser's default hash navigation, which fails to scroll or focus because the target step has display: none set.

Registering a click handler for navLinks inside the stepped mode block that maps the clicked module or step to the correct index and calls show(idx) will make navigation work seamlessly in both modes.

.wb-recap { text-align: center; padding: var(--wb-sp-8) 0; }
.wb-recap__badge { font-size: 2.5rem; color: var(--wb-good); }
.wb-recap h2 { font-size: 1.9rem; letter-spacing: var(--wb-h-tracking); }
.wb-recap[hidden] { display: none; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 P3 (minor) — Inline SVGs sharing the duplicate marker ID wb-arrow will conflict when multiple flow diagrams are generated on the same page. The browser will only use the first defined marker, which can cause rendering bugs if custom colors or styles are applied in later diagrams.

Incorporating a unique suffix (e.g., wb-arrow-{{id}}) dynamically during generation inside the workbook-generate command will prevent ID clashing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant