meeting-template

Meet Formatter v1.0 — Design Spec

Date: 2026-05-20 (drafted), 2026-05-21 (locator hardened against the real Calendar DOM) Status: Shipped — first public release Supersedes: an unreleased Manifest V2 prototype (root-level popup.html, popup.js, action.js, background.js, injector.js, app.js, bundled jQuery), all deleted as part of the v1.0 rewrite.

Note on versioning. Drafts of this spec used “v2” naming during the rewrite (because the prototype was conceptually v1 in development). For the first public release we renumbered to v1.0 — the prototype was never shipped. The product version in manifest.json is 1.0.0. References to “Manifest V2” elsewhere in this document refer to the deprecated Chrome extension manifest format, not to a product version.


1. Objective

A Chrome extension that lets a user define a single rich-text/Markdown template once and have it inserted into the Details field of any new Google Calendar event — automatically (when the toggle is on) or via a manual “Insert” button in the popup.

In scope

Non-goals (v1.0)


2. Architecture

Three runtime contexts, each with one clear job:

flowchart LR
    subgraph Extension["Chrome Extension (Manifest V3)"]
        direction TB

        subgraph Popup["Popup UI (Preact)"]
            App["App.tsx"]
            Editor["Editor.tsx<br/>(Tiptap StarterKit + History<br/>+ Placeholder + Markdown shortcuts)"]
            Toolbar["Toolbar.tsx<br/>B / I / U / • / 1. / H"]
            Settings["SettingsRow.tsx<br/>Auto-insert toggle"]
            Actions["ActionBar.tsx<br/>Save | Insert"]

            App --> Editor
            App --> Toolbar
            App --> Settings
            App --> Actions
        end

        subgraph SW["Service Worker (background)"]
            Router["Message router"]
            TabFinder["Active calendar tab finder"]
            Router --> TabFinder
        end

        subgraph Content["Content Script<br/>(matches calendar.google.com)"]
            Observer["waitForEventEditor()<br/>MutationObserver"]
            Locator["findDetailsEditor()"]
            Injector["injectSanitizedHtml()"]

            Observer --> Locator
            Locator --> Injector
        end

        subgraph Lib["lib/ (pure, unit-tested)"]
            Storage["storage.ts<br/>chrome.storage.sync facade"]
            Markdown["markdown.ts<br/>marked + DOMPurify"]
            DetailsLoc["details-locator.ts<br/>pure DOM helpers"]
        end

        Popup -- "load / save<br/>{template, autoInsert}" --> Storage
        Popup -- "INSERT_TEMPLATE msg" --> Router
        Router -- "tabs.sendMessage" --> Injector
        Injector -- "uses" --> Markdown
        Injector -- "uses" --> DetailsLoc
        Observer -- "uses" --> DetailsLoc
        Observer -- "reads autoInsert" --> Storage
    end

    Injector -.writes sanitized HTML.-> Calendar[("calendar.google.com<br/>Details contenteditable")]

    classDef pure fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef ui fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bg fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef cs fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef external fill:#f3f4f6,stroke:#6b7280,color:#374151

    class App,Editor,Toolbar,Settings,Actions ui
    class Router,TabFinder bg
    class Observer,Locator,Injector cs
    class Storage,Markdown,DetailsLoc pure
    class Calendar external

Module layout

src/
├── popup/
│   ├── App.tsx           # Top-level Preact component
│   ├── Editor.tsx        # Tiptap editor wrapper
│   ├── Toolbar.tsx       # B / I / U / • / 1. / H buttons
│   ├── SettingsRow.tsx   # Auto-insert toggle
│   ├── ActionBar.tsx     # Save | Insert
│   ├── popup.html        # Vite entry
│   └── popup.css         # Notion-ish design tokens
├── background/
│   └── service-worker.ts # Message router; dispatches INSERT_TEMPLATE
├── content/
│   └── calendar-injector.ts # Runs on calendar.google.com
├── lib/
│   ├── storage.ts        # chrome.storage.sync facade
│   ├── markdown.ts       # markdownToHtml(md) -> sanitized HTML
│   └── details-locator.ts # Pure DOM helpers
├── messages.ts           # Shared message-type constants & TS types
└── manifest.config.ts    # @crxjs/vite-plugin manifest

tests/
├── fixtures/
│   ├── fake-calendar.html        # E2E target
│   └── calendar-dialog-snapshot.html # Locator unit-test fixture
├── e2e/
│   └── *.spec.ts                 # Playwright E2E specs
└── verify-live.spec.ts           # Claude-driven live verification

Boundaries — why this split

Build & toolchain

Scripts

npm run … Effect
dev Vite dev with HMR; dist/ is load-unpackable
build Production build into dist/
test Vitest run once
test:watch Vitest watch mode
test:e2e Playwright E2E against fake Calendar
test:live Claude-driven verification against real Calendar (requires --user-data-dir)
typecheck tsc --noEmit
lint eslint src tests

3. Data model & storage

type Settings = {
  template: string        // markdown source, NOT rendered HTML
  autoInsert: boolean     // default: false
}

Stored in chrome.storage.sync so settings roam with the user’s Chrome profile.

Why store Markdown source, not HTML

  1. Roundtrip-safe — the editor loads it back into Tiptap without HTML→Markdown drift.
  2. Portable — Markdown is the lingua franca for export or future syncing.
  3. Sanitization at insert time — the on-disk format is human-readable and not pre-sanitized.

Auto-save mechanics

Save button behavior

Flushes the pending debounce immediately, then briefly swaps its label to Saved ✓ for 1.2 s before reverting to Save.


4. Auto-insert detection on Google Calendar

This is the riskiest part — Google can change the DOM. The implementation was hardened against real Google Calendar DOM samples taken during development (see Appendix A and Appendix B for the captured markup). Class names like JyrDof and id values like c4092 are minified and unstable; the stable anchors below are not.

Two event-creation surfaces

Calendar exposes two distinct UIs for creating an event. The extension must work in both. The differences are not cosmetic:

  Inline modal Full-page editor
Opens via Click on a calendar time-slot grid cell Keyboard c, “More options” from modal, or direct URL navigation
URL unchanged (/r/week etc.) /r/eventedit?overrides=...
Wrapper [role="dialog"] none — anchors live at document level
Description starts collapsed (must click [data-key="description"] to expand) already expanded; editor present at page load
Editor aria-label "Add description" "Description" (singular)
Common anchors #xDescIn, [contenteditable="true"][role="textbox"] same

Real DOM shape

Collapsed (Details has not been opened yet) — modal surface only:

<div role="dialog" aria-label="Create event">
  ...
  <span>
    Add
    <span class="JyrDof" data-key="description" jslog="39830">description</span>
    or
    <span class="JyrDof" data-key="attachments" jslog="64574">a Google Drive attachment</span>
  </span>
  <div data-expandable jsname="Ofl2hc" jscontroller="OHz5R">
    <button jsname="Zqjuqb" aria-expanded="false">Add description</button>
    <div jsname="xpDKHf" hidden> <!-- editor lives here once expanded --> </div>
  </div>
</div>

Expanded (after the user — or the extension — clicks the expander):

<div data-expandable jsname="Ofl2hc" class="... sMVRZe">
  <button jsname="Zqjuqb" aria-expanded="true">Add description</button>
  <div jsname="xpDKHf">
    <div role="toolbar" aria-label="Formatting options"> ... </div>
    <div id="xDescIn">
      <div aria-hidden="true">Add description</div>  <!-- placeholder span -->
      <div role="textbox"
           aria-multiline="true"
           aria-label="Add description"
           contenteditable="true"
           g_editable="true"
           id="hj99tb..." >
        <div><br></div>
      </div>
    </div>
  </div>
</div>

Stable anchors (in order of preference)

Description (the injection target):

  1. #xDescIn — the container element id is hardcoded by Google (“X Description Input”). Present on both surfaces when the description editor is mounted.
  2. Inside #xDescIn: [contenteditable="true"][role="textbox"].
  3. [contenteditable="true"][role="textbox"][aria-label="Description"] — full-page editor’s editor (singular Description).
  4. [contenteditable="true"][role="textbox"][aria-label="Add description"] — modal editor (only after expansion).
  5. [data-key="description"] — the span Google uses to open the description panel from the collapsed state (modal flow only).
  6. A <button> whose visible text is Add description — fallback expander (modal flow only).

Title (the refocus target):

  1. input#xTiIn — hardcoded id on the full-page editor.
  2. input[aria-label="Title"] — full-page editor (singular).
  3. input[aria-label="Add title"] — modal editor.
  4. input[placeholder="Add title"] — last-resort fallback shared by both surfaces.

role, aria-label, aria-multiline, g_editable, data-key, and jslog are tracking/accessibility hooks Google does not minify; the implementation relies only on these and the hardcoded id.

Locator API

findDetailsEditor(root: HTMLElement = document.body): HTMLElement | null
  // Synchronous; returns the editor if it's reachable inside root, else null.
  // Pass `[role="dialog"]` for the modal surface; pass `document.body` (default)
  // for the full-page editor. Strategy: #xDescIn → contenteditable, then
  // [role=textbox] with either aria-label, then legacy aria-label wrappers.

findDescriptionExpander(root: HTMLElement = document.body): HTMLElement | null
  // Synchronous; returns the click target that will open the panel (modal only):
  // [data-key="description"] first, then a button reading "Add description".
  // Returns null on the full-page editor (no expander needed).

findTitleField(root: HTMLElement = document.body): HTMLInputElement | null
  // Synchronous; returns the event Title input on either surface. Resolution
  // order: #xTiIn, then aria-label="Title", then aria-label="Add title",
  // then placeholder="Add title" as a last-resort fallback.

waitForEventEditor(root: HTMLElement = document.body, opts): Promise<HTMLElement | null>
  // Detects EITHER an inline modal ([role="dialog"]) OR the full-page editor
  // (a description anchor already at document level). Resolves the surface root.

waitFor(predicate, { timeoutMs, intervalMs }): Promise<T | null>
  // Generic polling helper used to wait for the editor after clicking the expander.

Runtime flow (content script)

  1. Match pattern*://calendar.google.com/* in the manifest content_scripts. The same content script runs on both /r/week (modal flow) and /r/eventedit (full-page flow) since both URLs match. Plus host_permissions: ["*://calendar.google.com/*"] (see §7) so the service worker can resolve the calendar tab when forwarding the INSERT_TEMPLATE message.

  2. findSurface() — picks the right search root for the current page:
    • If document.querySelector('[role="dialog"]') exists → return the dialog (modal flow).
    • Else if findDetailsEditor(document.body) || findDescriptionExpander(document.body) → return document.body (full-page flow).
    • Else → null (no event-creation surface is open).
  3. MutationObserver on document.body — watches for either surface appearing. The callback runs attemptAutoInsert(). The observer is also fired once on attach so the full-page editor (whose editor exists before the observer attaches at document_idle) is detected immediately.

  4. ensureAndInject({ markdown, manual }) — async:
    1. const surface = findSurface(). If null, return false.
    2. findDetailsEditor(surface). If found, jump to step 6.
    3. findDescriptionExpander(surface). If null, return false.
    4. Click the expander. waitFor(() => findDetailsEditor(surface), { timeoutMs: 2000 }).
    5. If still no editor after timeout, return false.
    6. Apply injection (empty-check, once-per-session guard, sanitize, set innerHTML, dispatch input).
  5. Empty check — only auto-insert if isEditorEmpty(editor) (the editor’s text is empty AND no meaningful structural children exist; <div><br></div> counts as empty — that’s Calendar’s initial state).

  6. Once-per-session guardeditor.dataset.meetFormatterInserted = 'true' after inserting; the observer’s callback respects this flag for the auto path so a single dialog session never gets double-filled.

  7. Input eventeditor.dispatchEvent(new Event('input', { bubbles: true })) so Calendar’s own change detection saves the description.

  8. Refocus the Title field — immediately after a successful injection, call findTitleField() and .focus() on the result. The caret is placed at the end of any existing title text via setSelectionRange(end, end) so the user can continue typing the event name. This pairs cleanly with the keyboard-driven flow (c → editor opens → template fills → focus is already in Title → user types name → Save). No-op if the title field can’t be located on the current surface.

Manual Insert (toggle off) runs the same flow but skips the empty-check and the once-per-session guard — explicit user action wins. When the editor is non-empty and manual=true, content is appended rather than replaced (insertAdjacentHTML('beforeend', html)), which preserves whatever the user had typed.

Known v1.0 limitation: editing existing events on the full-page editor

The full-page editor mounts the description contenteditable into the DOM before Calendar populates its content from the server (typical for “edit existing event”). The content script’s MutationObserver fires the moment the editor exists, but at that instant the editor’s innerHTML is <div><br></div> (empty). By the time Calendar populates the existing description, the once-per-session guard has already flipped — auto-insert is skipped, but the user’s existing content remains intact.

In practice this means: on the full-page editor, auto-insert may briefly race with Calendar’s content population; the empty-check still prevents overwrites because the content script writes the template and Calendar then writes back the server content (winning). The user can always click the popup’s manual Insert to append.

A clean fix (waiting for Calendar to settle before deciding) is reserved for v1.1 along with the rest of the edit-existing-event work in §10.


5. Markdown → HTML → DOM

Tiptap doc ─► serialize to MD ─► chrome.storage.sync
                                          │
                                          ▼
                              {INSERT_TEMPLATE, md}
                                          │
                                          ▼
                       marked(md) ─► raw HTML string
                                          │
                                          ▼
                       DOMPurify.sanitize(html, {
                         ALLOWED_TAGS: ['p','br','strong','em','u',
                                        'h1','h2','h3','ul','ol','li','a'],
                         ALLOWED_ATTR: ['href']
                       })
                                          │
                                          ▼
                       Range.insertNode + dispatch 'input' event

The input event is required — Calendar’s editor listens to it for autosave. Without it, the inserted content is visible but not persisted on Save.


6. Visual design

Notion-ish warm direction, applied as concrete tokens:

Token Value
Popup width 380 px
Popup max height 600 px (Chrome cap)
Background #fdfcfa
Surface (editor) #ffffff
Border 1px solid #e9e6e0
Text primary #37352f
Text muted (placeholder) #b5b2ab
Toolbar button bg #f1ede6
Toggle on #2eaadc
Primary button bg #37352f (Insert)
Secondary button white bg, #e9e6e0 border, #37352f text (Save)
Radius 6 px general, 9 px toggle, 12 px popup root
Font -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
Body font size 13 px popup chrome, 12 px editor body, 11 px toolbar
Shadow 0 4px 16px rgba(0,0,0,0.06) on popup root

Layout (top → bottom)

  1. Header — 📅 icon + “Meet Formatter” wordmark.
  2. Settings row — “Auto-insert on event open” label + toggle (right-aligned).
  3. ToolbarB I U • 1. H (left-aligned, rounded buttons).
  4. Editor body — Tiptap, shows Markdown supported placeholder when empty.
  5. Action bar — right-aligned: Save then Insert (Save on the left of Insert, per spec).

Toolbar buttons

Show active state (filled background #37352f with white glyph) when the cursor is inside the matching Tiptap mark. Each is also accessible via standard keyboard shortcut (⌘B, ⌘I, ⌘U, etc.) — Tiptap StarterKit provides these.

Accessibility


7. Manifest V3 manifest (sketch)

{
  "manifest_version": 3,
  "name": "Meet Formatter",
  "version": "1.0.0",
  "description": "Inject a Markdown-defined template into Google Calendar event Details.",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background/service-worker.ts", "type": "module" },
  "content_scripts": [
    {
      "matches": ["*://calendar.google.com/*"],
      "js": ["content/calendar-injector.ts"],
      "run_at": "document_idle"
    }
  ],
  "permissions": ["storage", "scripting"],
  "host_permissions": ["*://calendar.google.com/*"],
  "icons": {
    "16": "assets/images/icon16.png",
    "48": "assets/images/icon48.png",
    "128": "assets/images/icon128.png"
  }
}

host_permissions (not just activeTab) is required so the service worker’s chrome.tabs.query({ url: '*://calendar.google.com/*' }) actually returns the tab’s URL — without host access, Chrome omits the url field from tabs.query results and the message router can’t find the target tab.

The scripting permission lets the service worker call chrome.scripting.executeScript to programmatically inject the content script into Calendar tabs that were open before the extension was installed or reloaded. Without this, the user sees "Could not establish connection. Receiving end does not exist." after every extension update until they manually reload each Calendar tab. The service worker:

The @crxjs/vite-plugin generates the final manifest.json from this TypeScript config.


8. Testing strategy

Layer 1 — Unit (Vitest + JSDOM)

Colocated with source as *.test.ts. Target: <1 s total.

Spec What it asserts
lib/markdown.test.ts Markdown → sanitized HTML; strips <script>, onerror, javascript: URLs
lib/storage.test.ts Round-trips Settings; surfaces quota errors via mocked chrome.storage.sync
lib/details-locator.test.ts Loads fixture HTML snapshot of the Calendar event dialog; locator finds the right contenteditable and ignores decoys (e.g. the Title field)
popup/Editor.test.tsx Typing **bold** produces a Bold mark; undo/redo work via editor.commands.*
popup/App.test.tsx Save button flushes; auto-save fires 400 ms after onUpdate; Insert posts the right message

Layer 2 — E2E against fake Calendar (Playwright)

Runs against tests/fixtures/fake-calendar.html — a static page that mirrors the real Calendar’s collapsed-then-expand Description shape ([data-key="description"] span + jsname="Ofl2hc" expandable container + #xDescIn panel containing [role="textbox"][contenteditable="true"][aria-label="Add description"]). Deterministic, runs in CI, no Google login required.

HSTS workaround. Chrome force-upgrades calendar.google.com to HTTPS via its built-in HSTS preload list, regardless of the URL scheme the test passes. To still run the content script (whose match pattern is *://calendar.google.com/*) against a local fixture:

Cases (in tests/e2e/):

  1. Popup loads with the empty editor and Markdown supported placeholder.
  2. Typing auto-saves; closing and reopening the popup shows the same content.
  3. Save button flashes Saved ✓ confirmation.
  4. Auto-insert fills an empty Details panel when the toggle is on (exercises the click-to-expand path against [data-key="description"]).
  5. Auto-insert does not overwrite non-empty Details (test seeds prefilled content via the fixture’s window.__openDialogWithDescription(html) hook).
  6. Auto-insert is skipped when the toggle is off.
  7. Manual Insert appends to existing Details content rather than replacing.

Layer 3 — Claude-driven verification against real calendar.google.com

Runs once, manually, before declaring the task done. There are two viable harnesses; we use the second because it’s friction-free for the developer.

Option A — Playwright headed (kept as a checked-in spec, gated by RUN_LIVE=1). tests/verify-live.spec.ts launches Chromium with the unpacked extension and a persistent user-data dir. Requires the user to log into Google manually on first run.

Option B — Claude-driven via the Claude_in_Chrome MCP (preferred). Claude pairs with the user’s already-authenticated Chrome via the Claude_in_Chrome extension and the Control_Chrome / Claude_in_Chrome MCP tools. Steps:

  1. User installs the unpacked extension (dist/) into their normal Chrome and authorizes the Claude_in_Chrome extension to access calendar.google.com.
  2. Claude calls list_connected_browsersselect_browsertabs_context_mcp to grab a handle on a tab.
  3. Claude navigates to https://calendar.google.com/calendar/u/0/r, clicks the calendar grid (or the Create button → Event) to open the event dialog, takes a screenshot.
  4. Claude calls javascript_tool to run the same locator code the extension ships — document.querySelector('#xDescIn [contenteditable="true"][role="textbox"]') — and asserts the returned element matches the expected anchors.
  5. Claude verifies auto-insert fires (the Calendar editor now contains the rendered template) and takes a second screenshot.
  6. Claude saves the event, reloads, opens it, and confirms the description persisted — third screenshot.

The harness produces a short verbal report (“auto-insert fired, template rendered as expected, description survived save+reload”) plus the three screenshots. Failure modes are concrete: locator returns null (DOM has shifted — update §4 anchors), or the click on [data-key="description"] doesn’t expand the panel (Google has changed the expander handler — update §4 step 3).


9. Migration & cleanup

Files to delete from the current Manifest V2 codebase (all at repo root):

Files to keep:


10. Future work (next iteration — not built in v1.0)

Both are explicitly out of scope for v1.0 but called out here so the v1.0 data model and architecture leave room for them without rework (template: string becomes templates: Record<string, string> + activeId: string; the empty-check in §4 becomes a mode switch).


Appendix A — Captured Google Calendar DOM (2026-05-20)

Two snippets captured during development. These are the source of truth for the §4 locator anchors.

A.1 — Collapsed “Add description or attachment” prompt

<span id="c4092">
  Add
  <span jsaction="clickonly:s4Pcbe; mousedown:nzqhhd"
        class="JyrDof" data-key="description" jslog="39830">description</span>
  or
  <span jsaction="clickonly:s4Pcbe; mousedown:nzqhhd"
        class="JyrDof" data-key="attachments" jslog="64574">a Google Drive attachment</span>
</span>

The id="c4092" is dynamic (regenerated per session) and not used by the locator. The stable anchor is [data-key="description"].

A.2 — Expanded Description panel (after clicking the prompt)

<div data-expandable jsshadow class="anMZof AHjck dBA1M sMVRZe"
     data-uid="c4094" jsname="Ofl2hc" jscontroller="OHz5R">
  <div class="rdgVoe" jsname="L9hiGd" jsslot>
    <div class="HZL2hc DX1o3" jsname="Zqjuqb" jscontroller="K8MFQc">
      <button aria-expanded="false" aria-controls="c4094" ...>Add description</button>
    </div>
  </div>
  <div class="drQEgd" jsname="xpDKHf" jsslot id="c4094">
    ...
    <div jsname="L9AdLc" jslog="39830; track:OCqEwd;">
      <div jsname="INgbqf" role="toolbar" aria-label="Formatting options">
        <div role="button" aria-label="Bold" data-tooltip="Bold" aria-pressed="false" ... ></div>
        <div role="button" aria-label="Italic"   ...></div>
        <div role="button" aria-label="Underline"...></div>
        <div role="button" aria-label="Numbered list"...></div>
        <div role="button" aria-label="Bulleted list"...></div>
        <div role="button" aria-label="Insert link"...></div>
        <div role="button" aria-label="Remove formatting"...></div>
      </div>
      <div id="xDescIn" class="tAVIoc I9OJHe">
        <div jsname="V67aGc" class="GB6BTc snByac" aria-hidden="true">Add description</div>
        <div jsname="yrriRe"
             class="hj99tb KRoqRc editable"
             role="textbox"
             aria-multiline="true"
             aria-label="Add description"
             id="hj99tb2"
             g_editable="true"
             contenteditable="true"
             style="direction: ltr;">
          <div><br></div>
        </div>
      </div>
    </div>
  </div>
</div>

What this confirms:


Appendix B — Captured Google Calendar full-page editor DOM (2026-05-21)

Captured by pasting tests/manual-probe.js into DevTools console at https://calendar.google.com/calendar/u/0/r/eventedit?overrides=... (the URL Calendar navigates to when the user presses c from the calendar grid).

B.1 — Critical observation: no [role="dialog"]

The first probe returned hasDialog: false. The full-page editor is not wrapped in a [role="dialog"] container. This invalidated an earlier version of the locator which scoped all queries to document.querySelector('[role="dialog"]'). The locator was widened to default to document.body.

B.2 — The contenteditable element

A second probe at document level confirmed #xDescIn exists and the contenteditable inside it has these attributes (captured verbatim from the live page):

jsname            = "yrriRe"
jsaction          = "touchend:ufphYd; input:q3884e; paste:QD1hyc; drop:HZC9qb"
class             = "hj99tb KRoqRc editable"
role              = "textbox"
aria-multiline    = "true"
aria-label        = "Description"           ← singular, NOT "Add description"
id                = "hj99tb0"               ← varies, do not depend on
g_editable        = "true"
contenteditable   = "true"
style             = "direction: ltr;"

innerHTML on first open: <div><br></div> (same empty marker as the modal surface).

B.3 — No expander on this surface

findDescriptionExpander(document.body) returned null — the editor is already mounted, no click required. This is the principal behavioral difference between the two surfaces and is encoded in §4’s runtime flow (the findSurface()findDetailsEditor(surface) happy path skips straight to injection on the full-page editor).

B.4 — Title input (focus target after injection)

Captured from the same /r/eventedit page, the Title <input> is:

<input jsname="YPqjbf"
       type="text"
       value=""
       id="xTiIn"
       class="Fgl6fe-fmcmS-wGMbrd"
       jsaction="input:YPqjbf;focus:AHmuwe;blur:O22p3e"
       aria-controls="xTiIn-help-text-id"
       aria-describedby="xTiIn-help-text-id"
       aria-label="Title"
       placeholder="Add title"
       dir="auto"
       autofocus
       autocomplete="off">

Stable anchors: id="xTiIn" (hardcoded by Google), aria-label="Title" (singular). The modal surface has a similar input but with aria-label="Add title" and a dynamic id="c###".

B.5 — Skill used to reach this surface

A user-provided skill documents the keyboard path:

1. t  → today
2. d  → day view
3. c  → open full event editor (navigates to /r/eventedit)
4. Click "Add description" field; type description text
5. Click Save (top right)

The probe script tests/manual-probe.js documents the procedure for capturing future DOM samples when Calendar’s structure shifts.