@@ -358,6 +358,24 @@ async function exportDocument(mode: 'raw' | 'clean'): Promise<void> {
358358// Rendering
359359// ---------------------------------------------------------------------------
360360
361+ /**
362+ * Scroll the first control carrying `tag` into view. Dogfoods the shipped
363+ * public API: resolve the control id by tag (`selectByTag`), then
364+ * `ui.contentControls.scrollIntoView`. Scroll-only - it does not move the
365+ * cursor into the control (focus/activate is a separate concern).
366+ */
367+ function locateByTag ( tag : string ) : void {
368+ const ui = state . ui ;
369+ const editor = state . editor ;
370+ if ( ! ui || ! editor ?. doc ) return ;
371+ const { items } = editor . doc . contentControls . selectByTag ( { tag } ) ;
372+ const first = items [ 0 ] ;
373+ if ( ! first ) return ;
374+ // `target.nodeId` is the SDT node id (= the painted `data-sdt-id`), which is
375+ // what scrollIntoView matches on.
376+ void ui . contentControls . scrollIntoView ( { id : first . target . nodeId , block : 'center' } ) ;
377+ }
378+
361379function renderPanels ( ) : void {
362380 renderFieldsPanel ( ) ;
363381 renderClausesPanel ( ) ;
@@ -366,13 +384,23 @@ function renderPanels(): void {
366384function renderFieldsPanel ( ) : void {
367385 fieldsPanelEl . innerHTML = '' ;
368386 for ( const field of FIELDS ) {
369- const row = document . createElement ( 'label' ) ;
387+ // A <div> wrapper (not <label>): a <label> may not contain interactive
388+ // content, so the Locate <button> must be a sibling of the input, with a
389+ // real <label for> tying the field name to the input.
390+ const row = document . createElement ( 'div' ) ;
370391 row . className = 'row' ;
392+ const inputId = `field-${ field . key } ` ;
371393 row . innerHTML = `
372- <span class="row-label">${ escapeHtml ( field . label ) } </span>
373- <input data-field="${ field . key } " value="${ escapeAttr ( state . values [ field . key ] ?? '' ) } " />
394+ <div class="row-label">
395+ <label class="row-label-text" for="${ inputId } ">${ escapeHtml ( field . label ) } </label>
396+ <button class="locate" type="button" data-locate-field="${ escapeAttr ( field . key ) } " aria-label="Locate ${ escapeAttr ( field . label ) } in the document" title="Scroll to this field">Locate</button>
397+ </div>
398+ <input id="${ inputId } " data-field="${ field . key } " value="${ escapeAttr ( state . values [ field . key ] ?? '' ) } " />
374399 ` ;
375400 fieldsPanelEl . appendChild ( row ) ;
401+ row . querySelector < HTMLButtonElement > ( '.locate' ) ?. addEventListener ( 'click' , ( ) => {
402+ locateByTag ( fieldTag ( field . key ) ) ;
403+ } ) ;
376404 const input = row . querySelector < HTMLInputElement > ( 'input' ) ;
377405 if ( ! input ) continue ;
378406 // Reactive: each keystroke debounces ~250ms and fans the value to every
@@ -404,7 +432,10 @@ function renderClausesPanel(): void {
404432 card . innerHTML = `
405433 <header class="clause-header">
406434 <h3 class="clause-label">${ escapeHtml ( clause . label ) } </h3>
407- <span class="clause-status">Update available</span>
435+ <div class="clause-actions">
436+ <span class="clause-status">Update available</span>
437+ <button class="locate" type="button" data-locate-clause="${ escapeAttr ( clause . id ) } " aria-label="Locate ${ escapeAttr ( clause . label ) } in the document" title="Scroll to this clause">Locate</button>
438+ </div>
408439 </header>
409440 <p class="clause-summary">${ escapeHtml ( upgrade . summary ) } </p>
410441 <p class="clause-meta">Document ${ escapeHtml ( inDoc ) } \u00b7 Library ${ escapeHtml ( upgrade . version ) } </p>
@@ -435,12 +466,18 @@ function renderClausesPanel(): void {
435466 card . innerHTML = `
436467 <header class="clause-header">
437468 <h3 class="clause-label">${ escapeHtml ( clause . label ) } </h3>
438- <span class="clause-status muted">Current</span>
469+ <div class="clause-actions">
470+ <span class="clause-status muted">Current</span>
471+ <button class="locate" type="button" data-locate-clause="${ escapeAttr ( clause . id ) } " aria-label="Locate ${ escapeAttr ( clause . label ) } in the document" title="Scroll to this clause">Locate</button>
472+ </div>
439473 </header>
440474 <p class="clause-meta">Document ${ escapeHtml ( inDoc ) } </p>
441475 ` ;
442476 }
443477
478+ card . querySelector < HTMLButtonElement > ( '.locate' ) ?. addEventListener ( 'click' , ( ) => {
479+ locateByTag ( clauseTag ( clause . id , inDoc ) ) ;
480+ } ) ;
444481 clausesPanelEl . appendChild ( card ) ;
445482 }
446483}
0 commit comments