Skip to content

feat(synced-tabs): synced OPTION tab groups (openspec: tutorial-synced-option-tabs) - #3023

Merged
jung-thomas merged 12 commits into
DEVfrom
worktree-synced-option-tabs
Nov 6, 2026
Merged

jung-thomas merged 12 commits into
DEVfrom
worktree-synced-option-tabs

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

What

Implements the tutorial-synced-option-tabs openspec: OPTION tab groups authored with sync=runtime (or sync=account, sync=ide) synchronize their selection across all strips on a page and across page navigations via localStorage. Authors write:

```markdown
[OPTION BEGIN [Node.js] sync=runtime]
Node.js content
[OPTION END]
[OPTION BEGIN [Java] sync=runtime]
Java content
[OPTION END]
```

And readers who pick "Java" once see Java everywhere on the current tutorial and on the next one they visit.

Why

Replaces the current experience where every OPTION group resets to its first tab independently. The runtime preset (Node.js/Java), account preset (Trial/Enterprise), and ide preset (BAS/VS Code) are the most-clicked tabs in tutorial analytics — users were re-clicking on every step.

Key pieces

  • scripts/parsers/sync-presets.ts — alias/canonicalization tables (Node → Node.js, etc.)
  • scripts/parsers/options.ts — sync= parser; emits data-sync-key/data-sync-value attrs and syncValues= canonical list on the strip
  • hugo/layouts/shortcodes/option-tabs.html + tab.html — WAI-ARIA tab components with sync attributes
  • hugo/assets/js/sync-groups.ts — unified localStorage runtime; same-page fan-out via CustomEvent, cross-tab via storage event; resolveChoice order: URL param > stored > page default
  • hugo/assets/js/tutorial.ts — wires initSyncedTabs to apply stored/URL-driven choice on load, propagate missing-variant placeholder per More options rendering problems #2820
  • hugo/layouts/tutorials/u1-object-page.html — page-level picker bar (one segmented button per sync key, gated on syncKeysUsed)
  • packages/mcp/option-filter.js + CDS changes — MCP get_tutorial_step optional variants: String param to filter OPTION blocks server-side
  • docs/authors/writing-tutorials.md — §3.5.4 "Synced option tabs ★ NEW" with preset table and deep-link pattern
  • docs/samples/meta-tutorials/synced-option-tabs/tutorial.md — draft meta sample (in-PR only; publish to sap-tutorials/meta-tutorials batched after all fan-out features land)

How tested

  • Parser tests: 22 tests in options-sync.test.ts (preset canonicalization, explicit/auto/override/error paths)
  • Regression: options-sync-regression.test.ts — 16 tests pin byte-identical output for no-sync tutorials including a full toBe(EXPECTED) on the complete document
  • Sync-groups unit: 33 tests in sync-groups.test.ts including DOM integration for picker bar, same-page CustomEvent fan-out, and missing-variant placeholder
  • MCP slicer: 18 tests in tutorial-step-slicer-variants.test.js for variant filtering
  • openspec validate: tutorial-synced-option-tabs — all 16 task checkboxes marked complete
  • e2e: test/e2e/synced-tabs.spec.ts — 4 self-skipping Playwright tests (skips without SMOKE_BASE_URL; run post-DEV-deploy)

Deploy scope

Backend + frontend + docs. No content rebuild needed (parser changes apply on next fetch-tutorials run). No schema changes.

Notes

  • No new runtime npm dependency
  • Backward compat: no-sync tutorials produce byte-identical parser output (pinned by regression test)
  • The meta sample under docs/samples/meta-tutorials/synced-option-tabs/ is for review only — not published to sap-tutorials/meta-tutorials yet
  • e2e spec self-skips in CI (requires SMOKE_BASE_URL); run manually post-DEV-deploy

…dapters

- Add hugo/assets/js/sync-groups.ts: getChoice/setChoice (unified JSON object
  at localStorage["sync-groups"]), onChange (cross-tab storage events with
  unsubscribe), resolveChoice (5-step URL>stored>detected>default>firstTab).
  Migration read-fallback for legacy 'os-preference' and 'codetabs-preference'
  keys so returning readers' stored choices are honoured.
- Refactor hugo/assets/js/os-toggle.ts: delegates getPreference/setPreference
  and cross-tab sync to sync-groups; preserves osprefchange dispatch, all
  detection logic (BAS/clientHints/UA), activate/data-os-active, #2820
  fallback strip, pickInitialOs, and __test__ exports unchanged.
- Refactor hugo/assets/js/codetabs.ts: delegates getPreference/setPreference
  and cross-tab onChange to sync-groups under key 'codetabs'; codetabs-preference
  legacy fallback active for one release.
- Add test/unit/sync-groups.test.ts: 30 happy-dom unit tests covering
  get/set round-trip, corrupt-storage safety, migration fallbacks (os +
  codetabs), onChange cross-tab fire + unsubscribe, resolveChoice full
  5-step order with case-insensitive canonical URL match.
- Extend test/unit/os-toggle.test.ts: 3 new legacy-migration cases confirming
  detectDefaultOs() still resolves legacy os-preference via sync-groups adapter.
Empty string stored under a key must not short-circuit resolution and
skip detected/groupDefault/firstTab. Task 3 writes arbitrary OPTION tab
values; an empty write would have stranded the picker permanently blank.

- sync-groups.ts: `if (stored !== null && stored !== '') return stored;`
- sync-groups.test.ts: regression test — stored "" falls through to
  detected, then groupDefault, then firstTab.
…hortcode params

- Extends OPTION BEGIN regex to capture optional sync=<key> per block
- Adds OptionSyncError for conflicting keys in one group and undeclared keys
- Adds syncTabs/syncOverrides/syncGroups/syncKeysUsedOut to ConvertOptions
- Resolves sync key with priority: explicit > forced > auto-classify
- Emits sync="<key>" on option-tabs and syncValue="<canonical>" on each tab
- Preset keys: runtime/account/ide; custom keys via syncGroups frontmatter
- Types: TutorialFrontmatter gains syncTabs, syncOverrides, syncGroups
- ComposeResult gains syncKeysUsed + syncGroups forwarding
- render-frontmatter: emits syncKeysUsed[] + syncGroups to Hugo .Params
- fetch-tutorials: threads syncKeysUsed/syncGroups through writeHugoPage
- Backward compat: no sync= and no syncTabs:auto → byte-identical shortcode output
…add line numbers to OptionSyncError

- I2: validate explicit sync=<key> against presets/syncGroups BEFORE the
  syncOverrides:'none' early-return; typo'd undeclared keys must always fail
  the build even when that step's sync is disabled
- I1: thread firstMatch.index through resolveGroupSyncKey via new
  lineOfIndex() helper; both conflicting-key and undeclared-key errors now
  include "at line N" in the message
- m2: tighten backward-compat test with full toBe assertion on the exact
  emitted shortcode string to catch capture-group numbering regressions
- Test: add case for sync=db (undeclared) + syncOverrides:{slug:'none'}
  → still throws OptionSyncError naming db
When a syncKey is present, compute canonical sync values for every tab
in the group and emit them as a new syncValues= parameter on the
option-tabs shortcode. The template reads syncValues[i] for each button's
data-sync-value so it agrees with the panel's data-sync-value (which already
used the canonical value via computeSyncValue). Without this fix, a label
like "Node" produced data-sync-value="Node" on the button but "Node.js" on
the panel, breaking client-side sync for any label that gets canonicalised.

Also updates the Task 3 shape-assertion test to expect the new syncValues=
parameter, and adds a new describe block proving the button/panel agreement
for the canonicalising "Node" → "Node.js" case.
- sync-groups.ts: add initPickerBars() which wires click handlers on
  [data-sync-picker] elements, hides pickers when < 2 strips share a key,
  updates is-selected/hidden state on all matching .option-tabs strips, and
  writes the choice to the unified localStorage object.
- sync-groups.ts: applyChoiceToStrips() injects a [data-sync-missing-placeholder]
  div when a strip lacks the active data-sync-value, naming the missing variant
  and listing available ones — no silent substitution.
- sync-groups.ts: announceChoice() writes to [data-sync-announce] aria-live
  region after every picker change for assistive-technology support.
- u1-object-page.html: add sync-group picker bar block (gated on syncKeysUsed,
  one ui5-segmented-button per key, values from syncGroups.values) after OS picker.
- u1-object-page.html: add js.Build sync-groups.ts <script defer> block gated on
  syncKeysUsed + not previewMode, after the os-toggle script block.
- test/unit/sync-groups.test.ts: 4 new DOM-integration tests covering picker
  activation, missing-variant placeholder, single-strip hide, and aria-live.
Task 7 deliverables:
- scripts/parsers/__tests__/options-sync-regression.test.ts: 15 tests proving
  no-sync tutorials produce byte-identical output and emit no sync attrs;
  gate check confirms same fixture DOES classify with syncTabs:auto
- docs/authors/writing-tutorials.md: add §3.5.4 "Synced option tabs" with
  preset table, usage examples, auto-classification tip, deep links, custom keys
- docs/samples/meta-tutorials/synced-option-tabs/tutorial.md: 3-step sample
  tutorial demonstrating sync=runtime across Node.js/Java groups (draft, not published)
- test/e2e/synced-tabs.spec.ts: self-skipping e2e covering sync selection,
  deep link pre-selection, cross-page persistence, missing-variant fallback
- openspec/changes/tutorial-synced-option-tabs/tasks.md: check off 2.5, 3.3, 4.3, 4.4
Add optional `variants` parameter to `get_tutorial_step` MCP tool so callers
can filter OPTION blocks in markdown source to a preferred runtime/account/ide
variant. Implemented as a pure filter in `packages/mcp/option-filter.js` with
inline sync-preset canonicalisation (mirrors sync-presets.ts, no CDS deps).

- packages/mcp/option-filter.js (new): pure `filterVariants(markdown, variants)`
  with parseSegments grouping logic; inline SYNC_PRESETS tables from sync-presets.ts
- packages/mcp/tutorial-step-slicer.js: import filterVariants, add optional
  third arg to sliceStepMarkdown, re-export filterVariants
- packages/mcp/index.js: barrel re-export filterVariants
- packages/mcp/mcp-developer-tools.js: extract req.data.variants (JSON-encoded
  map), pass to sliceStepMarkdown; fail-open metrics counter on active filter
- srv/lib/tutorial-step-slicer.js: re-export filterVariants from shim
- srv/developer-service-mcp.cds: add variants: String param to get_tutorial_step
- srv/search-service-mcp.cds: same
- srv/mcp/mcp-manifest.js: update tool description to mention variants
- test/unit/tutorial-step-slicer-variants.test.js (new): 9 pure-unit tests for
  filterVariants covering matched/unmatched/empty/no-sync/mixed/plain-text cases
@jung-thomas
jung-thomas merged commit 562ff44 into DEV Nov 6, 2026
6 of 7 checks passed
@jung-thomas
jung-thomas deleted the worktree-synced-option-tabs branch November 6, 2026 05:58
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