Skip to content

Therapy Pack — Tools → "Add to Editor" Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Let a clinician push a tool's current results into a therapy pack — a uniform "Add to Editor" control on the shared results toolbar (Word Lists, Contrast Sets, Sentences) plus a single-word add in Lookup — with a pack-picker (choose a saved pack or "New pack…") and a confirmation toast with Undo (spec §5.7 + §15).

Architecture: The shared SelectionToolbar already carries every grid tool's results as data: ExportData + dataType + selection indices — the one uniform surface. A pure toPackItems(data, dataType, indices, sourceTool) adapter maps those shapes to PackItem[] (words carry image refs → picture cards). An AddToEditorButton (button + pack-picker menu) activates the chosen pack and bulk-adds via new store actions addItems/removeItems (Undo), showing a toast. The button drops into SelectionToolbar (covers 3 tools) and into LookupTool (single word).

Tech Stack: React 19, TypeScript, MUI 7, Zustand 5, Vitest 4 + @testing-library/react. Reuses the pack store, PackSourceTool, and the ExportData contract.

Global Constraints

  • Store is the single mutation surface. The control calls store actions (addItems, removeItems, loadPack, newPack); it never touches packDb.
  • Uniform contract via ExportData. SelectionToolbar's data: ExportData (Word[] | MinimalPair[] | ContrastiveGroup[] | CorpusMatch[]) + dataType: 'words'|'pairs'|'groups'|'sentences' is the single mapping surface for the three grid tools. Do NOT add per-tool result plumbing.
  • Add the SELECTED items (SelectionToolbar only shows on selection). Map data filtered by selectedIndices.
  • Words link to the lexicon at add-time. Tool Word objects already carry ipa/image_file/image_provider/has_image (from the search API) — map them straight onto PackWord.imageFile/imageProvider/ipa/hasImage (no re-fetch). (spec §15)
  • Pack-picker, not always-active. "Add to Editor" opens a menu of saved packs (from usePackStore.library) + "New pack…"; the pick becomes the active pack, items are added, the pack indicator reflects it. (spec §15)
  • Confirmation toast with Undo. After add: a MUI Snackbar "Added N to " with an Undo action that removes the just-added items. (spec §5.7)</li> <li><strong><code>PackSourceTool</code></strong> already includes <code>wordLists</code>, <code>contrastive</code>, <code>sentences</code>, <code>lookup</code>, <code>textAnalysis</code> (<code>src/types/therapyPack.ts</code>). Use the right one per surface.</li> <li><strong>No new analytics events</strong> (taxonomy is the next phase). <strong>Speech Analysis and Text Analysis are out of scope</strong> for this phase (audio stays outside the pack MVP; Text-Analysis add is deferred — noted).</li> <li><strong>Test commands</strong> (from <code>packages/web/frontend/</code>): <code>npm test -- --run <file></code>, <code>npm run type-check</code>, <code>npm run lint</code>, <code>npm run build</code>.</li> </ul> <hr /> <h2 id="file-structure">File Structure<a class="headerlink" href="#file-structure" title="Permanent link">¶</a></h2> <ul> <li><code>src/store/packStore.ts</code> (modify) — <code>addItems(items)</code> + <code>removeItems(ids)</code> actions.</li> <li><code>src/lib/pack/toPackItems.ts</code> (create) — <code>ExportData</code> → <code>PackItem[]</code> adapter.</li> <li><code>src/lib/pack/toPackItems.test.ts</code> (create).</li> <li><code>src/components/pack/AddToEditorButton.tsx</code> (create) — button + pack-picker menu + toast/Undo.</li> <li><code>src/components/pack/AddToEditorButton.test.tsx</code> (create).</li> <li><code>src/components/shared/SelectionToolbar.tsx</code> (modify) — render <code>AddToEditorButton</code> from <code>data</code>/<code>dataType</code>/<code>selectedIndices</code>.</li> <li><code>src/components/shared/SelectionToolbar.test.tsx</code> (create or modify).</li> <li><code>src/components/tools/LookupTool.tsx</code> (modify) — single-word <code>AddToEditorButton</code>.</li> </ul> <hr /> <h3 id="task-1-store-additems-removeitems">Task 1: Store <code>addItems</code> + <code>removeItems</code><a class="headerlink" href="#task-1-store-additems-removeitems" title="Permanent link">¶</a></h3> <p><strong>Files:</strong> - Modify: <code>src/store/packStore.ts</code> - Test: <code>src/store/packStore.test.ts</code></p> <p><strong>Interfaces:</strong> - Produces (added to <code>PackState</code>): - <code>addItems: (items: PackItem[]) => void</code> — appends each item to its section (<code>word</code>/<code>contrast</code>/<code>sentence</code>) and commits <strong>once</strong>; no-op on empty; no-op if no active pack. - <code>removeItems: (ids: string[]) => void</code> — removes every listed id across all sections in one commit; no-op on empty / no active pack.</p> <ul> <li>[ ] <strong>Step 1: Write the failing tests</strong></li> </ul> <p>Add to <code>src/store/packStore.test.ts</code>:</p> <div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'addItems appends a mixed batch in one commit'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">newPack</span><span class="p">();</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">addItems</span><span class="p">([</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w1'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'contrast'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'c1'</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="s1">'key'</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="o">:</span><span class="w"> </span><span class="s1">'tea'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'contrastive'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentence'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'s1'</span><span class="p">,</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="s1">'A rocket.'</span><span class="p">,</span><span class="w"> </span><span class="nx">targets</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">]);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">;</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">words</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">contrasts</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">sentences</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'removeItems drops all listed ids across sections'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">newPack</span><span class="p">();</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">addItems</span><span class="p">([</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w1'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'a'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w2'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'b'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentence'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'s1'</span><span class="p">,</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="s1">'x'</span><span class="p">,</span><span class="w"> </span><span class="nx">targets</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">]);</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">removeItems</span><span class="p">([</span><span class="s1">'w1'</span><span class="p">,</span><span class="w"> </span><span class="s1">'s1'</span><span class="p">]);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">;</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">words</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">id</span><span class="p">)).</span><span class="nx">toEqual</span><span class="p">([</span><span class="s1">'w2'</span><span class="p">]);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">sentences</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">0</span><span class="p">);</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'addItems is a no-op with no active pack'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span><span class="w"> </span><span class="nx">activePack</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">addItems</span><span class="p">([{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'x'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'manual'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}]);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="p">).</span><span class="nx">toBeNull</span><span class="p">();</span> <span class="w"> </span><span class="p">});</span> </code></pre></div> <ul> <li>[ ] <strong>Step 2: Run the tests to verify they fail</strong></li> </ul> <p>Run: <code>npm test -- --run src/store/packStore.test.ts</code> Expected: FAIL — <code>addItems</code>/<code>removeItems</code> undefined.</p> <ul> <li>[ ] <strong>Step 3: Implement</strong></li> </ul> <p>In <code>src/store/packStore.ts</code>, add to the <code>PackState</code> interface:</p> <p><div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">addItems</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">items</span><span class="o">:</span><span class="w"> </span><span class="kt">PackItem</span><span class="p">[])</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="ow">void</span><span class="p">;</span> <span class="w"> </span><span class="nx">removeItems</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">ids</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">[])</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="ow">void</span><span class="p">;</span> </code></pre></div> and implement (near <code>addItem</code>):</p> <div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">addItems</span><span class="p">(</span><span class="nx">items</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">get</span><span class="p">().</span><span class="nx">activePack</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">p</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">words</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[...</span><span class="nx">p</span><span class="p">.</span><span class="nx">words</span><span class="p">];</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">contrasts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[...</span><span class="nx">p</span><span class="p">.</span><span class="nx">contrasts</span><span class="p">];</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">sentences</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[...</span><span class="nx">p</span><span class="p">.</span><span class="nx">sentences</span><span class="p">];</span> <span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">it</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">items</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">it</span><span class="p">.</span><span class="nx">kind</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'word'</span><span class="p">)</span><span class="w"> </span><span class="nx">words</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">it</span><span class="p">);</span> <span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">it</span><span class="p">.</span><span class="nx">kind</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'contrast'</span><span class="p">)</span><span class="w"> </span><span class="nx">contrasts</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">it</span><span class="p">);</span> <span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="nx">sentences</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">it</span><span class="p">);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="nx">commit</span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="nx">p</span><span class="p">,</span><span class="w"> </span><span class="nx">words</span><span class="p">,</span><span class="w"> </span><span class="nx">contrasts</span><span class="p">,</span><span class="w"> </span><span class="nx">sentences</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="nx">removeItems</span><span class="p">(</span><span class="nx">ids</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">get</span><span class="p">().</span><span class="nx">activePack</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">p</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">ids</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">drop</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Set</span><span class="p">(</span><span class="nx">ids</span><span class="p">);</span> <span class="w"> </span><span class="nx">commit</span><span class="p">({</span> <span class="w"> </span><span class="p">...</span><span class="nx">p</span><span class="p">,</span> <span class="w"> </span><span class="nx">words</span><span class="o">:</span><span class="w"> </span><span class="kt">p.words.filter</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o">!</span><span class="nx">drop</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">id</span><span class="p">)),</span> <span class="w"> </span><span class="nx">contrasts</span><span class="o">:</span><span class="w"> </span><span class="kt">p.contrasts.filter</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o">!</span><span class="nx">drop</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">id</span><span class="p">)),</span> <span class="w"> </span><span class="nx">sentences</span><span class="o">:</span><span class="w"> </span><span class="kt">p.sentences.filter</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o">!</span><span class="nx">drop</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nx">id</span><span class="p">)),</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">},</span> </code></pre></div> <ul> <li>[ ] <strong>Step 4: Run the tests to verify they pass</strong></li> </ul> <p>Run: <code>npm test -- --run src/store/packStore.test.ts</code> Expected: PASS.</p> <ul> <li>[ ] <strong>Step 5: Type-check and commit</strong></li> </ul> <div class="highlight"><pre><span></span><code>npm<span class="w"> </span>run<span class="w"> </span>type-check git<span class="w"> </span>add<span class="w"> </span>src/store/packStore.ts<span class="w"> </span>src/store/packStore.test.ts git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">"feat(packs): addItems/removeItems bulk store actions (PHON-170)"</span> </code></pre></div> <hr /> <h3 id="task-2-topackitems-exportdata-packitem">Task 2: <code>toPackItems</code> — ExportData → PackItem[]<a class="headerlink" href="#task-2-topackitems-exportdata-packitem" title="Permanent link">¶</a></h3> <p><strong>Files:</strong> - Create: <code>src/lib/pack/toPackItems.ts</code> - Test: <code>src/lib/pack/toPackItems.test.ts</code></p> <p><strong>Interfaces:</strong> - Consumes: <code>ExportData</code> + <code>ContrastiveGroup</code> from <code>../../components/shared/ExportMenu</code>; <code>Word</code>/<code>MinimalPair</code> from <code>../../services/phonolexApi</code>; <code>CorpusMatch</code> from <code>../../types/governance</code>; <code>PackItem</code>/<code>PackWord</code>/<code>PackContrast</code>/<code>PackSentence</code>/<code>PackSourceTool</code> from <code>../../types/therapyPack</code>; <code>newId</code> from <code>../id</code>. - Produces: - <code>export function toPackItems(data: ExportData, dataType: 'words' | 'pairs' | 'groups' | 'sentences', indices: number[] | null, sourceTool: PackSourceTool, at: number): PackItem[]</code> — maps the <code>indices</code>-selected rows (or all rows when <code>indices</code> is null/empty) to pack items: - <code>words</code> → <code>PackWord</code> each (word, ipa, <code>image_file</code>→<code>imageFile</code>, <code>image_provider</code>→<code>imageProvider</code>, <code>has_image</code>→<code>hasImage</code>). - <code>pairs</code> → <code>PackContrast</code> each (<code>word1.word</code>, <code>word2.word</code>, label <code>"${phoneme1} vs ${phoneme2}"</code>). - <code>groups</code> → the group's words flattened to <code>PackWord[]</code>. - <code>sentences</code> → <code>PackSentence</code> each (text; targets = union of <code>highlights.include_surfaces</code> + <code>highlights.pair_surfaces</code>). - Every item carries <code>provenance: { sourceTool, addedAt: at }</code>.</p> <ul> <li>[ ] <strong>Step 1: Write the failing test</strong></li> </ul> <p>Create <code>src/lib/pack/toPackItems.test.ts</code>:</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">describe</span><span class="p">,</span><span class="w"> </span><span class="nx">it</span><span class="p">,</span><span class="w"> </span><span class="nx">expect</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'vitest'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">toPackItems</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./toPackItems'</span><span class="p">;</span> <span class="nx">describe</span><span class="p">(</span><span class="s1">'toPackItems'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'maps selected words with image refs'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">ipa</span><span class="o">:</span><span class="w"> </span><span class="s1">'ɹ'</span><span class="p">,</span><span class="w"> </span><span class="nx">has_image</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="p">,</span><span class="w"> </span><span class="nx">image_file</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket.svg'</span><span class="p">,</span><span class="w"> </span><span class="nx">image_provider</span><span class="o">:</span><span class="w"> </span><span class="s1">'mulberry'</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'skip'</span><span class="p">,</span><span class="w"> </span><span class="nx">ipa</span><span class="o">:</span><span class="w"> </span><span class="s1">'s'</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="s1">'words'</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="mf">0</span><span class="p">],</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="mf">5</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="mf">0</span><span class="p">]).</span><span class="nx">toMatchObject</span><span class="p">({</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">imageFile</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket.svg'</span><span class="p">,</span><span class="w"> </span><span class="nx">imageProvider</span><span class="o">:</span><span class="w"> </span><span class="s1">'mulberry'</span><span class="p">,</span><span class="w"> </span><span class="nx">hasImage</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">provenance</span><span class="p">).</span><span class="nx">toMatchObject</span><span class="p">({</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">5</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'maps all rows when indices is null'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'a'</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'b'</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">;</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="s1">'words'</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">)).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">2</span><span class="p">);</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'maps pairs to contrasts'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nx">word1</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'key'</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">word2</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'tea'</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">phoneme1</span><span class="o">:</span><span class="w"> </span><span class="s1">'k'</span><span class="p">,</span><span class="w"> </span><span class="nx">phoneme2</span><span class="o">:</span><span class="w"> </span><span class="s1">'t'</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="s1">'pairs'</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="s1">'contrastive'</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="mf">0</span><span class="p">]).</span><span class="nx">toMatchObject</span><span class="p">({</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'contrast'</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="s1">'key'</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="o">:</span><span class="w"> </span><span class="s1">'tea'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'k vs t'</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'flattens groups to words'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nx">words</span><span class="o">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'cat'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'bat'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="s1">'groups'</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="s1">'contrastive'</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span><span class="nx">i</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">}).</span><span class="nx">word</span><span class="p">)).</span><span class="nx">toEqual</span><span class="p">([</span><span class="s1">'cat'</span><span class="p">,</span><span class="w"> </span><span class="s1">'bat'</span><span class="p">]);</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'maps sentences with highlight targets'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="s1">'The key.'</span><span class="p">,</span><span class="w"> </span><span class="nx">highlights</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">include_surfaces</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'key'</span><span class="p">],</span><span class="w"> </span><span class="nx">pair_surfaces</span><span class="o">:</span><span class="w"> </span><span class="p">[]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="mf">0</span><span class="p">]).</span><span class="nx">toMatchObject</span><span class="p">({</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentence'</span><span class="p">,</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="s1">'The key.'</span><span class="p">,</span><span class="w"> </span><span class="nx">targets</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'key'</span><span class="p">]</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">});</span> <span class="p">});</span> </code></pre></div> <ul> <li>[ ] <strong>Step 2: Run the test to verify it fails</strong></li> </ul> <p>Run: <code>npm test -- --run src/lib/pack/toPackItems.test.ts</code> Expected: FAIL — module not found.</p> <ul> <li>[ ] <strong>Step 3: Implement</strong></li> </ul> <p>Create <code>src/lib/pack/toPackItems.ts</code>:</p> <div class="highlight"><pre><span></span><code><span class="cm">/**</span> <span class="cm"> * Adapter: a tool's ExportData (the uniform results contract shared by</span> <span class="cm"> * SelectionToolbar/ExportMenu) → pack items (spec §5.7 / §15). Words carry</span> <span class="cm"> * their lexicon image refs so they render as picture cards with no re-fetch.</span> <span class="cm"> */</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">ExportData</span><span class="p">,</span><span class="w"> </span><span class="nx">ContrastiveGroup</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../components/shared/ExportMenu'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Word</span><span class="p">,</span><span class="w"> </span><span class="nx">MinimalPair</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../services/phonolexApi'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">CorpusMatch</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../types/governance'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">PackItem</span><span class="p">,</span><span class="w"> </span><span class="nx">PackWord</span><span class="p">,</span><span class="w"> </span><span class="nx">PackSourceTool</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../types/therapyPack'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">newId</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../id'</span><span class="p">;</span> <span class="kd">function</span><span class="w"> </span><span class="nx">wordToPack</span><span class="p">(</span><span class="nx">w</span><span class="o">:</span><span class="w"> </span><span class="kt">Word</span><span class="p">,</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="kt">PackSourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">at</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="nx">PackWord</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span> <span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">newId</span><span class="p">(),</span> <span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="kt">w.word</span><span class="p">,</span> <span class="w"> </span><span class="nx">ipa</span><span class="o">:</span><span class="w"> </span><span class="kt">w.ipa</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span> <span class="w"> </span><span class="nx">hasImage</span><span class="o">:</span><span class="w"> </span><span class="kt">w.has_image</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">image_file</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nx">true</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="kt">undefined</span><span class="p">,</span> <span class="w"> </span><span class="nx">imageFile</span><span class="o">:</span><span class="w"> </span><span class="kt">w.image_file</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span> <span class="w"> </span><span class="nx">imageProvider</span><span class="o">:</span><span class="w"> </span><span class="kt">w.image_provider</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span> <span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">at</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">};</span> <span class="p">}</span> <span class="kd">function</span><span class="w"> </span><span class="nx">pick</span><span class="o"><</span><span class="nx">T</span><span class="o">></span><span class="p">(</span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="kt">T</span><span class="p">[],</span><span class="w"> </span><span class="nx">indices</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">[]</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="nx">T</span><span class="p">[]</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">indices</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">indices</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">rows</span><span class="p">;</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Set</span><span class="p">(</span><span class="nx">indices</span><span class="p">);</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">rows</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">set</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">i</span><span class="p">));</span> <span class="p">}</span> <span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span> <span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">ExportData</span><span class="p">,</span> <span class="w"> </span><span class="nx">dataType</span><span class="o">:</span><span class="w"> </span><span class="s1">'words'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'pairs'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'groups'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">,</span> <span class="w"> </span><span class="nx">indices</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">[]</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span> <span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="kt">PackSourceTool</span><span class="p">,</span> <span class="w"> </span><span class="nx">at</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">,</span> <span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="nx">PackItem</span><span class="p">[]</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dataType</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'words'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">pick</span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">Word</span><span class="p">[],</span><span class="w"> </span><span class="nx">indices</span><span class="p">).</span><span class="nx">map</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">wordToPack</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="nx">sourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">at</span><span class="p">));</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dataType</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'pairs'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">pick</span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">MinimalPair</span><span class="p">[],</span><span class="w"> </span><span class="nx">indices</span><span class="p">).</span><span class="nx">map</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span> <span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'contrast'</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="kd">const</span><span class="p">,</span> <span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">newId</span><span class="p">(),</span> <span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="kt">p.word1.word</span><span class="p">,</span> <span class="w"> </span><span class="nx">b</span><span class="o">:</span><span class="w"> </span><span class="kt">p.word2.word</span><span class="p">,</span> <span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">p</span><span class="p">.</span><span class="nx">phoneme1</span><span class="si">}</span><span class="sb"> vs </span><span class="si">${</span><span class="nx">p</span><span class="p">.</span><span class="nx">phoneme2</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">at</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dataType</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'groups'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">pick</span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">ContrastiveGroup</span><span class="p">[],</span><span class="w"> </span><span class="nx">indices</span><span class="p">).</span><span class="nx">flatMap</span><span class="p">((</span><span class="nx">g</span><span class="p">)</span><span class="w"> </span><span class="p">=></span> <span class="w"> </span><span class="nx">g</span><span class="p">.</span><span class="nx">words</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">gw</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">wordToPack</span><span class="p">(</span><span class="nx">gw</span><span class="p">.</span><span class="nx">word</span><span class="p">,</span><span class="w"> </span><span class="nx">sourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">at</span><span class="p">)),</span> <span class="w"> </span><span class="p">);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">pick</span><span class="p">(</span><span class="nx">data</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">CorpusMatch</span><span class="p">[],</span><span class="w"> </span><span class="nx">indices</span><span class="p">).</span><span class="nx">map</span><span class="p">((</span><span class="nx">m</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span> <span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'sentence'</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="kd">const</span><span class="p">,</span> <span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">newId</span><span class="p">(),</span> <span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="kt">m.text</span><span class="p">,</span> <span class="w"> </span><span class="nx">targets</span><span class="o">:</span><span class="w"> </span><span class="kt">Array.from</span><span class="p">(</span><span class="ow">new</span><span class="w"> </span><span class="nb">Set</span><span class="p">([</span> <span class="w"> </span><span class="p">...(</span><span class="nx">m</span><span class="p">.</span><span class="nx">highlights</span><span class="o">?</span><span class="p">.</span><span class="nx">include_surfaces</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">[]),</span> <span class="w"> </span><span class="p">...(</span><span class="nx">m</span><span class="p">.</span><span class="nx">highlights</span><span class="o">?</span><span class="p">.</span><span class="nx">pair_surfaces</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="p">[]),</span> <span class="w"> </span><span class="p">])),</span> <span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">at</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">}));</span> <span class="p">}</span> </code></pre></div> <p>(If <code>MinimalPair</code> / <code>ContrastiveGroup</code> field names differ from the above when you read <code>ExportMenu.tsx</code>/<code>phonolexApi</code>, adjust the field access to match the real types — the shapes are: <code>MinimalPair</code> has <code>word1</code>/<code>word2</code> (<code>Word</code>) + <code>phoneme1</code>/<code>phoneme2</code>; <code>ContrastiveGroup</code> has <code>words: { word: Word; phoneme; position }[]</code>.)</p> <ul> <li>[ ] <strong>Step 4: Run the test to verify it passes</strong></li> </ul> <p>Run: <code>npm test -- --run src/lib/pack/toPackItems.test.ts</code> Expected: PASS (5/5).</p> <ul> <li>[ ] <strong>Step 5: Type-check and commit</strong></li> </ul> <div class="highlight"><pre><span></span><code>npm<span class="w"> </span>run<span class="w"> </span>type-check git<span class="w"> </span>add<span class="w"> </span>src/lib/pack/toPackItems.ts<span class="w"> </span>src/lib/pack/toPackItems.test.ts git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">"feat(packs): toPackItems — ExportData → pack items adapter (PHON-170)"</span> </code></pre></div> <hr /> <h3 id="task-3-addtoeditorbutton-pack-picker-toastundo">Task 3: <code>AddToEditorButton</code> — pack-picker + toast/Undo<a class="headerlink" href="#task-3-addtoeditorbutton-pack-picker-toastundo" title="Permanent link">¶</a></h3> <p><strong>Files:</strong> - Create: <code>src/components/pack/AddToEditorButton.tsx</code> - Test: <code>src/components/pack/AddToEditorButton.test.tsx</code></p> <p><strong>Interfaces:</strong> - Consumes: <code>usePackStore</code> (<code>library</code>, <code>hydrate</code>, <code>loadPack</code>, <code>newPack</code>, <code>addItems</code>, <code>removeItems</code>, <code>activePack</code>); <code>PackItem</code> type. - Produces: <code>AddToEditorButton</code> props <code>{ getItems: () => PackItem[]; disabled?: boolean; size?: 'small' | 'medium'; label?: string }</code>. - A button ("Add to Editor" by default). On click: <code>hydrate()</code> then open a Menu listing each saved pack (title + counts) and a "New pack…" item. - Picking a saved pack → <code>loadPack(id)</code>; "New pack…" → <code>newPack()</code>. Then compute <code>const items = getItems()</code>, <code>addItems(items)</code>, remember <code>items.map(i => i.id)</code>, and show a Snackbar <code>Added ${items.length} to "${activePack.title}"</code> with an <strong>Undo</strong> action that calls <code>removeItems(addedIds)</code>. - If <code>getItems()</code> returns <code>[]</code>, no-op (no menu, or a disabled state).</p> <ul> <li>[ ] <strong>Step 1: Write the failing test</strong></li> </ul> <p>Create <code>src/components/pack/AddToEditorButton.test.tsx</code>:</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">describe</span><span class="p">,</span><span class="w"> </span><span class="nx">it</span><span class="p">,</span><span class="w"> </span><span class="nx">expect</span><span class="p">,</span><span class="w"> </span><span class="nx">beforeEach</span><span class="p">,</span><span class="w"> </span><span class="nx">vi</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'vitest'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">render</span><span class="p">,</span><span class="w"> </span><span class="nx">screen</span><span class="p">,</span><span class="w"> </span><span class="nx">fireEvent</span><span class="p">,</span><span class="w"> </span><span class="nx">waitFor</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@testing-library/react'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="nx">AddToEditorButton</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./AddToEditorButton'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">usePackStore</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../store/packStore'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">getAllPacks</span><span class="p">,</span><span class="w"> </span><span class="nx">deletePack</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../lib/packDb'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">PackItem</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../types/therapyPack'</span><span class="p">;</span> <span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">clearDb</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">all</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">getAllPacks</span><span class="p">();</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">all</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">p</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">deletePack</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">id</span><span class="p">)));</span> <span class="p">}</span> <span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="o">:</span><span class="w"> </span><span class="nx">PackItem</span><span class="p">[]</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">[</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w1'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">kind</span><span class="o">:</span><span class="w"> </span><span class="s1">'word'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="s1">'w2'</span><span class="p">,</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rabbit'</span><span class="p">,</span><span class="w"> </span><span class="nx">provenance</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">sourceTool</span><span class="o">:</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">,</span><span class="w"> </span><span class="nx">addedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span> <span class="p">];</span> <span class="nx">describe</span><span class="p">(</span><span class="s1">'AddToEditorButton'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">beforeEach</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">clearDb</span><span class="p">();</span> <span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span><span class="w"> </span><span class="nx">activePack</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="p">,</span><span class="w"> </span><span class="nx">library</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">saveError</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'creates a new pack and adds items via the menu'</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">render</span><span class="p">(<</span><span class="nt">AddToEditorButton</span><span class="w"> </span><span class="na">getItems</span><span class="o">=</span><span class="p">{</span><span class="nx">items</span><span class="p">}</span><span class="w"> </span><span class="p">/>);</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'menuitem'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/new pack/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="p">).</span><span class="nx">not</span><span class="p">.</span><span class="nx">toBeNull</span><span class="p">());</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">2</span><span class="p">);</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByText</span><span class="p">(</span><span class="sr">/added 2 to/i</span><span class="p">)).</span><span class="nx">toBeInTheDocument</span><span class="p">();</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'adds to an existing saved pack'</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">newPack</span><span class="p">();</span> <span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">setTitle</span><span class="p">(</span><span class="s1">'My /r/ pack'</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span> <span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span><span class="w"> </span><span class="nx">activePack</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">render</span><span class="p">(<</span><span class="nt">AddToEditorButton</span><span class="w"> </span><span class="na">getItems</span><span class="o">=</span><span class="p">{</span><span class="nx">items</span><span class="p">}</span><span class="w"> </span><span class="p">/>);</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'menuitem'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/my \/r\/ pack/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">id</span><span class="p">));</span> <span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">2</span><span class="p">);</span> <span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'Undo removes the just-added items'</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">render</span><span class="p">(<</span><span class="nt">AddToEditorButton</span><span class="w"> </span><span class="na">getItems</span><span class="o">=</span><span class="p">{</span><span class="nx">items</span><span class="p">}</span><span class="w"> </span><span class="p">/>);</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'menuitem'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/new pack/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">2</span><span class="p">));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/undo/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mf">0</span><span class="p">));</span> <span class="w"> </span><span class="p">});</span> <span class="p">});</span> </code></pre></div> <ul> <li>[ ] <strong>Step 2: Run the test to verify it fails</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/pack/AddToEditorButton.test.tsx</code> Expected: FAIL — module not found.</p> <ul> <li>[ ] <strong>Step 3: Implement</strong></li> </ul> <p>Create <code>src/components/pack/AddToEditorButton.tsx</code>:</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="nx">React</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">useState</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'react'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Button</span><span class="p">,</span><span class="w"> </span><span class="nx">Menu</span><span class="p">,</span><span class="w"> </span><span class="nx">MenuItem</span><span class="p">,</span><span class="w"> </span><span class="nx">Divider</span><span class="p">,</span><span class="w"> </span><span class="nx">ListItemText</span><span class="p">,</span><span class="w"> </span><span class="nx">Snackbar</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@mui/material'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">PlaylistAdd</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">AddIcon</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@mui/icons-material'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">usePackStore</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../store/packStore'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">PackItem</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../types/therapyPack'</span><span class="p">;</span> <span class="kd">interface</span><span class="w"> </span><span class="nx">AddToEditorButtonProps</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">getItems</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">PackItem</span><span class="p">[];</span> <span class="w"> </span><span class="nx">disabled?</span><span class="o">:</span><span class="w"> </span><span class="kt">boolean</span><span class="p">;</span> <span class="w"> </span><span class="nx">size</span><span class="o">?:</span><span class="w"> </span><span class="s1">'small'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'medium'</span><span class="p">;</span> <span class="w"> </span><span class="nx">label?</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span><span class="w"> </span><span class="nx">AddToEditorButton</span><span class="o">:</span><span class="w"> </span><span class="kt">React.FC</span><span class="p"><</span><span class="nt">AddToEditorButtonProps</span><span class="p">></span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span> <span class="w"> </span><span class="nx">getItems</span><span class="p">,</span><span class="w"> </span><span class="nx">disabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'small'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Add to Editor'</span><span class="p">,</span> <span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">library</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">library</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">anchor</span><span class="p">,</span><span class="w"> </span><span class="nx">setAnchor</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useState</span><span class="p"><</span><span class="nt">null</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="na">HTMLElement</span><span class="p">>(</span><span class="kc">null</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">snack</span><span class="p">,</span><span class="w"> </span><span class="nx">setSnack</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useState</span><span class="o"><</span><span class="p">{</span><span class="w"> </span><span class="nx">msg</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">ids</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">[]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kc">null</span><span class="o">></span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">open</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="o">:</span><span class="w"> </span><span class="kt">React.MouseEvent</span><span class="p"><</span><span class="nt">HTMLElement</span><span class="p">>)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="ow">void</span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">hydrate</span><span class="p">();</span> <span class="w"> </span><span class="nx">setAnchor</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">currentTarget</span><span class="p">);</span> <span class="w"> </span><span class="p">};</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">close</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">setAnchor</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">addTo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">activate</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="ow">void</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nb">Promise</span><span class="p"><</span><span class="nt">void</span><span class="p">>)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">close</span><span class="p">();</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getItems</span><span class="p">();</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">activate</span><span class="p">();</span> <span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">addItems</span><span class="p">(</span><span class="nx">items</span><span class="p">);</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">?</span><span class="p">.</span><span class="nx">title</span><span class="w"> </span><span class="o">??</span><span class="w"> </span><span class="s1">'pack'</span><span class="p">;</span> <span class="w"> </span><span class="nx">setSnack</span><span class="p">({</span><span class="w"> </span><span class="nx">msg</span><span class="o">:</span><span class="w"> </span><span class="sb">`Added </span><span class="si">${</span><span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="si">}</span><span class="sb"> to "</span><span class="si">${</span><span class="nx">title</span><span class="si">}</span><span class="sb">"`</span><span class="p">,</span><span class="w"> </span><span class="nx">ids</span><span class="o">:</span><span class="w"> </span><span class="kt">items.map</span><span class="p">((</span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">i</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">});</span> <span class="w"> </span><span class="p">};</span> <span class="w"> </span><span class="p">};</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span> <span class="w"> </span><span class="p"><></span> <span class="w"> </span><span class="p"><</span><span class="nt">Button</span><span class="w"> </span><span class="na">size</span><span class="o">=</span><span class="p">{</span><span class="nx">size</span><span class="p">}</span><span class="w"> </span><span class="na">variant</span><span class="o">=</span><span class="s">"outlined"</span><span class="w"> </span><span class="na">startIcon</span><span class="o">=</span><span class="p">{<</span><span class="nt">AddIcon</span><span class="w"> </span><span class="p">/>}</span><span class="w"> </span><span class="na">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">disabled</span><span class="p">}</span><span class="w"> </span><span class="na">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">open</span><span class="p">}></span> <span class="w"> </span><span class="p">{</span><span class="nx">label</span><span class="p">}</span> <span class="w"> </span><span class="p"></</span><span class="nt">Button</span><span class="p">></span> <span class="w"> </span><span class="p"><</span><span class="nt">Menu</span><span class="w"> </span><span class="na">anchorEl</span><span class="o">=</span><span class="p">{</span><span class="nx">anchor</span><span class="p">}</span><span class="w"> </span><span class="na">open</span><span class="o">=</span><span class="p">{</span><span class="nb">Boolean</span><span class="p">(</span><span class="nx">anchor</span><span class="p">)}</span><span class="w"> </span><span class="na">onClose</span><span class="o">=</span><span class="p">{</span><span class="nx">close</span><span class="p">}></span> <span class="w"> </span><span class="p"><</span><span class="nt">MenuItem</span><span class="w"> </span><span class="na">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">addTo</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">newPack</span><span class="p">())}></span> <span class="w"> </span><span class="p"><</span><span class="nt">ListItemText</span><span class="w"> </span><span class="na">primary</span><span class="o">=</span><span class="s">"New pack…"</span><span class="w"> </span><span class="p">/></span> <span class="w"> </span><span class="p"></</span><span class="nt">MenuItem</span><span class="p">></span> <span class="w"> </span><span class="p">{</span><span class="nx">library</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="mf">0</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="p"><</span><span class="nt">Divider</span><span class="w"> </span><span class="p">/>}</span> <span class="w"> </span><span class="p">{</span><span class="nx">library</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span> <span class="w"> </span><span class="p"><</span><span class="nt">MenuItem</span><span class="w"> </span><span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">s</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="w"> </span><span class="na">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">addTo</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">loadPack</span><span class="p">(</span><span class="nx">s</span><span class="p">.</span><span class="nx">id</span><span class="p">))}></span> <span class="w"> </span><span class="p"><</span><span class="nt">ListItemText</span> <span class="w"> </span><span class="na">primary</span><span class="o">=</span><span class="p">{</span><span class="nx">s</span><span class="p">.</span><span class="nx">title</span><span class="p">}</span> <span class="w"> </span><span class="na">secondary</span><span class="o">=</span><span class="p">{</span><span class="sb">`</span><span class="si">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">counts</span><span class="p">.</span><span class="nx">words</span><span class="si">}</span><span class="sb"> words · </span><span class="si">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">counts</span><span class="p">.</span><span class="nx">contrasts</span><span class="si">}</span><span class="sb"> contrasts · </span><span class="si">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">counts</span><span class="p">.</span><span class="nx">sentences</span><span class="si">}</span><span class="sb"> sentences`</span><span class="p">}</span> <span class="w"> </span><span class="p">/></span> <span class="w"> </span><span class="p"></</span><span class="nt">MenuItem</span><span class="p">></span> <span class="w"> </span><span class="p">))}</span> <span class="w"> </span><span class="p"></</span><span class="nt">Menu</span><span class="p">></span> <span class="w"> </span><span class="p"><</span><span class="nt">Snackbar</span> <span class="w"> </span><span class="na">open</span><span class="o">=</span><span class="p">{</span><span class="nb">Boolean</span><span class="p">(</span><span class="nx">snack</span><span class="p">)}</span> <span class="w"> </span><span class="na">autoHideDuration</span><span class="o">=</span><span class="p">{</span><span class="mf">6000</span><span class="p">}</span> <span class="w"> </span><span class="na">onClose</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">setSnack</span><span class="p">(</span><span class="kc">null</span><span class="p">)}</span> <span class="w"> </span><span class="na">message</span><span class="o">=</span><span class="p">{</span><span class="nx">snack</span><span class="o">?</span><span class="p">.</span><span class="nx">msg</span><span class="p">}</span> <span class="w"> </span><span class="na">action</span><span class="o">=</span><span class="p">{</span> <span class="w"> </span><span class="p"><</span><span class="nt">Button</span><span class="w"> </span><span class="na">color</span><span class="o">=</span><span class="s">"secondary"</span><span class="w"> </span><span class="na">size</span><span class="o">=</span><span class="s">"small"</span><span class="w"> </span><span class="na">onClick</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">snack</span><span class="p">)</span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">removeItems</span><span class="p">(</span><span class="nx">snack</span><span class="p">.</span><span class="nx">ids</span><span class="p">);</span> <span class="w"> </span><span class="nx">setSnack</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="w"> </span><span class="p">}}></span> <span class="w"> </span><span class="nx">Undo</span> <span class="w"> </span><span class="p"></</span><span class="nt">Button</span><span class="p">></span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">/></span> <span class="w"> </span><span class="p"></></span> <span class="w"> </span><span class="p">);</span> <span class="p">};</span> <span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">AddToEditorButton</span><span class="p">;</span> </code></pre></div> <ul> <li>[ ] <strong>Step 4: Run the test to verify it passes</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/pack/AddToEditorButton.test.tsx</code> Expected: PASS (3/3).</p> <ul> <li>[ ] <strong>Step 5: Commit</strong></li> </ul> <div class="highlight"><pre><span></span><code>git<span class="w"> </span>add<span class="w"> </span>src/components/pack/AddToEditorButton.tsx<span class="w"> </span>src/components/pack/AddToEditorButton.test.tsx git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">"feat(packs): AddToEditorButton — pack-picker menu + toast/undo (PHON-170)"</span> </code></pre></div> <hr /> <h3 id="task-4-wire-add-to-editor-into-selectiontoolbar-word-lists-contrast-sets-sentences">Task 4: Wire "Add to Editor" into <code>SelectionToolbar</code> (Word Lists · Contrast Sets · Sentences)<a class="headerlink" href="#task-4-wire-add-to-editor-into-selectiontoolbar-word-lists-contrast-sets-sentences" title="Permanent link">¶</a></h3> <p><strong>Files:</strong> - Modify: <code>src/components/shared/SelectionToolbar.tsx</code> - Test: <code>src/components/shared/SelectionToolbar.test.tsx</code> (create if absent)</p> <p><strong>Interfaces:</strong> - <code>SelectionToolbar</code> renders <code><AddToEditorButton getItems={() => toPackItems(data, dataType, selectedIndices, sourceToolFor(dataType), Date.now())} /></code> alongside its existing <code>ExportMenu</code>. - Add a helper <code>sourceToolFor(dataType)</code>: <code>'words'</code>→<code>'wordLists'</code>, <code>'pairs'|'groups'</code>→<code>'contrastive'</code>, <code>'sentences'</code>→<code>'sentences'</code>.</p> <ul> <li>[ ] <strong>Step 1: Write the failing test</strong></li> </ul> <p>Create/extend <code>src/components/shared/SelectionToolbar.test.tsx</code>. Read the current <code>SelectionToolbar</code> render to supply required props (<code>totalCount</code>, <code>selectedCount</code>, <code>selectedIndices</code>, <code>onSelectAll</code>, <code>onClearAll</code>, <code>data</code>, <code>dataType</code>). Because the toolbar renders only when <code>selectedCount > 0</code>, pass a non-zero selection.</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">describe</span><span class="p">,</span><span class="w"> </span><span class="nx">it</span><span class="p">,</span><span class="w"> </span><span class="nx">expect</span><span class="p">,</span><span class="w"> </span><span class="nx">beforeEach</span><span class="p">,</span><span class="w"> </span><span class="nx">vi</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'vitest'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">render</span><span class="p">,</span><span class="w"> </span><span class="nx">screen</span><span class="p">,</span><span class="w"> </span><span class="nx">fireEvent</span><span class="p">,</span><span class="w"> </span><span class="nx">waitFor</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@testing-library/react'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="nx">SelectionToolbar</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./SelectionToolbar'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">usePackStore</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../store/packStore'</span><span class="p">;</span> <span class="nx">describe</span><span class="p">(</span><span class="s1">'SelectionToolbar — Add to Editor'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">beforeEach</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span><span class="w"> </span><span class="nx">activePack</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="p">,</span><span class="w"> </span><span class="nx">library</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">saveError</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'adds the selected words to a new pack'</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">render</span><span class="p">(</span> <span class="w"> </span><span class="p"><</span><span class="nt">SelectionToolbar</span> <span class="w"> </span><span class="na">totalCount</span><span class="o">=</span><span class="p">{</span><span class="mf">2</span><span class="p">}</span><span class="w"> </span><span class="na">selectedCount</span><span class="o">=</span><span class="p">{</span><span class="mf">1</span><span class="p">}</span><span class="w"> </span><span class="na">selectedIndices</span><span class="o">=</span><span class="p">{[</span><span class="mf">0</span><span class="p">]}</span> <span class="w"> </span><span class="na">onSelectAll</span><span class="o">=</span><span class="p">{</span><span class="nx">vi</span><span class="p">.</span><span class="nx">fn</span><span class="p">()}</span><span class="w"> </span><span class="na">onClearAll</span><span class="o">=</span><span class="p">{</span><span class="nx">vi</span><span class="p">.</span><span class="nx">fn</span><span class="p">()}</span> <span class="w"> </span><span class="na">data</span><span class="o">=</span><span class="p">{[{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">ipa</span><span class="o">:</span><span class="w"> </span><span class="s1">'ɹ'</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'skip'</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">}</span> <span class="w"> </span><span class="na">dataType</span><span class="o">=</span><span class="s">"words"</span> <span class="w"> </span><span class="p">/>,</span> <span class="w"> </span><span class="p">);</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'menuitem'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/new pack/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">w</span><span class="p">.</span><span class="nx">word</span><span class="p">)).</span><span class="nx">toEqual</span><span class="p">([</span><span class="s1">'rocket'</span><span class="p">]));</span> <span class="w"> </span><span class="p">});</span> <span class="p">});</span> </code></pre></div> <ul> <li>[ ] <strong>Step 2: Run the test to verify it fails</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/shared/SelectionToolbar.test.tsx</code> Expected: FAIL — no "Add to Editor" button.</p> <ul> <li>[ ] <strong>Step 3: Implement</strong></li> </ul> <p>In <code>src/components/shared/SelectionToolbar.tsx</code>: import the button + adapter, add the helper, render the button next to <code>ExportMenu</code> (read the file to place it in the same action row). Map only the selected rows via <code>selectedIndices</code>.</p> <p><div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="nx">AddToEditorButton</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../pack/AddToEditorButton'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">toPackItems</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../lib/pack/toPackItems'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">PackSourceTool</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../types/therapyPack'</span><span class="p">;</span> <span class="kd">function</span><span class="w"> </span><span class="nx">sourceToolFor</span><span class="p">(</span><span class="nx">dataType</span><span class="o">:</span><span class="w"> </span><span class="s1">'words'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'pairs'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'groups'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="nx">PackSourceTool</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dataType</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'words'</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s1">'wordLists'</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">dataType</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s1">'sentences'</span><span class="p">;</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="s1">'contrastive'</span><span class="p">;</span> <span class="p">}</span> </code></pre></div> In the toolbar's action area (next to <code><ExportMenu .../></code>): <div class="highlight"><pre><span></span><code><span class="w"> </span><span class="p"><</span><span class="nt">AddToEditorButton</span> <span class="w"> </span><span class="na">getItems</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="nx">dataType</span><span class="p">,</span><span class="w"> </span><span class="nx">selectedIndices</span><span class="p">,</span><span class="w"> </span><span class="nx">sourceToolFor</span><span class="p">(</span><span class="nx">dataType</span><span class="p">),</span><span class="w"> </span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">())}</span> <span class="w"> </span><span class="p">/></span> </code></pre></div></p> <ul> <li>[ ] <strong>Step 4: Run the test to verify it passes</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/shared/SelectionToolbar.test.tsx</code> Expected: PASS.</p> <ul> <li>[ ] <strong>Step 5: Commit</strong></li> </ul> <div class="highlight"><pre><span></span><code>git<span class="w"> </span>add<span class="w"> </span>src/components/shared/SelectionToolbar.tsx<span class="w"> </span>src/components/shared/SelectionToolbar.test.tsx git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">"feat(packs): Add to Editor on the shared results toolbar (word lists/contrasts/sentences) (PHON-170)"</span> </code></pre></div> <hr /> <h3 id="task-5-lookup-single-word-add-to-editor-full-gate">Task 5: Lookup single-word "Add to Editor" + full gate<a class="headerlink" href="#task-5-lookup-single-word-add-to-editor-full-gate" title="Permanent link">¶</a></h3> <p><strong>Files:</strong> - Modify: <code>src/components/tools/LookupTool.tsx</code> - Test: <code>src/components/tools/LookupTool.test.tsx</code> (create if absent; otherwise a focused new test)</p> <p><strong>Interfaces:</strong> - In <code>LookupTool</code>, when <code>wordResult</code> (a <code>Word</code>) is present, render <code><AddToEditorButton getItems={() => toPackItems([wordResult], 'words', null, 'lookup', Date.now())} /></code> near the word header/detail.</p> <ul> <li>[ ] <strong>Step 1: Write the failing test</strong></li> </ul> <p>Read <code>LookupTool.tsx</code> to find how to render it with a populated <code>wordResult</code> (it fetches on a query; the test can mock <code>api.getWord</code>). Create <code>src/components/tools/LookupTool.test.tsx</code>:</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">describe</span><span class="p">,</span><span class="w"> </span><span class="nx">it</span><span class="p">,</span><span class="w"> </span><span class="nx">expect</span><span class="p">,</span><span class="w"> </span><span class="nx">beforeEach</span><span class="p">,</span><span class="w"> </span><span class="nx">vi</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'vitest'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">render</span><span class="p">,</span><span class="w"> </span><span class="nx">screen</span><span class="p">,</span><span class="w"> </span><span class="nx">fireEvent</span><span class="p">,</span><span class="w"> </span><span class="nx">waitFor</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'@testing-library/react'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="nx">LookupTool</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./LookupTool'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="nx">api</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../services/phonolexApi'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">usePackStore</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../../store/packStore'</span><span class="p">;</span> <span class="nx">describe</span><span class="p">(</span><span class="s1">'LookupTool — Add to Editor'</span><span class="p">,</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">beforeEach</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span><span class="w"> </span><span class="nx">activePack</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="p">,</span><span class="w"> </span><span class="nx">library</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nx">saveError</span><span class="o">:</span><span class="w"> </span><span class="kt">null</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">it</span><span class="p">(</span><span class="s1">'adds the looked-up word to a new pack'</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">vi</span><span class="p">.</span><span class="nx">spyOn</span><span class="p">(</span><span class="nx">api</span><span class="p">,</span><span class="w"> </span><span class="s1">'getWord'</span><span class="p">).</span><span class="nx">mockResolvedValue</span><span class="p">({</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">ipa</span><span class="o">:</span><span class="w"> </span><span class="s1">'ɹ'</span><span class="p">,</span><span class="w"> </span><span class="nx">has_image</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="p">,</span><span class="w"> </span><span class="nx">image_file</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket.svg'</span><span class="p">,</span><span class="w"> </span><span class="nx">image_provider</span><span class="o">:</span><span class="w"> </span><span class="s1">'mulberry'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">never</span><span class="p">);</span> <span class="w"> </span><span class="nx">render</span><span class="p">(<</span><span class="nt">LookupTool</span><span class="w"> </span><span class="na">initialWord</span><span class="o">=</span><span class="s">"rocket"</span><span class="w"> </span><span class="p">/>);</span> <span class="w"> </span><span class="c1">// wait for the word to load, then Add to Editor</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">})).</span><span class="nx">toBeInTheDocument</span><span class="p">());</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">screen</span><span class="p">.</span><span class="nx">getByRole</span><span class="p">(</span><span class="s1">'button'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/add to editor/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="k">await</span><span class="w"> </span><span class="nx">screen</span><span class="p">.</span><span class="nx">findByRole</span><span class="p">(</span><span class="s1">'menuitem'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="sr">/new pack/i</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">waitFor</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">expect</span><span class="p">(</span><span class="nx">usePackStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">activePack</span><span class="o">!</span><span class="p">.</span><span class="nx">words</span><span class="p">[</span><span class="mf">0</span><span class="p">]).</span><span class="nx">toMatchObject</span><span class="p">({</span><span class="w"> </span><span class="nx">word</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket'</span><span class="p">,</span><span class="w"> </span><span class="nx">imageFile</span><span class="o">:</span><span class="w"> </span><span class="s1">'rocket.svg'</span><span class="w"> </span><span class="p">}));</span> <span class="w"> </span><span class="p">});</span> <span class="p">});</span> </code></pre></div> <p>(If <code>LookupTool</code> doesn't accept <code>initialWord</code> or fetches differently, adapt the test to drive a lookup through its input — read the component first. The assertion that matters: a loaded word + Add to Editor → new pack contains that word with its image ref.)</p> <ul> <li>[ ] <strong>Step 2: Run the test to verify it fails</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/tools/LookupTool.test.tsx</code> Expected: FAIL — no "Add to Editor" button.</p> <ul> <li>[ ] <strong>Step 3: Implement</strong></li> </ul> <p>In <code>src/components/tools/LookupTool.tsx</code>, import <code>AddToEditorButton</code> + <code>toPackItems</code>, and render the button in the word-result header block (guarded by <code>wordResult</code>):</p> <div class="highlight"><pre><span></span><code><span class="p">{</span><span class="nx">wordResult</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="p">(</span> <span class="w"> </span><span class="p"><</span><span class="nt">AddToEditorButton</span><span class="w"> </span><span class="na">getItems</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">toPackItems</span><span class="p">([</span><span class="nx">wordResult</span><span class="p">],</span><span class="w"> </span><span class="s1">'words'</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="s1">'lookup'</span><span class="p">,</span><span class="w"> </span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">())}</span><span class="w"> </span><span class="p">/></span> <span class="p">)}</span> </code></pre></div> <ul> <li>[ ] <strong>Step 4: Run the test to verify it passes</strong></li> </ul> <p>Run: <code>npm test -- --run src/components/tools/LookupTool.test.tsx</code> Expected: PASS.</p> <ul> <li>[ ] <strong>Step 5: Full gate</strong></li> </ul> <p>Run (from <code>packages/web/frontend/</code>): <div class="highlight"><pre><span></span><code>npm<span class="w"> </span><span class="nb">test</span><span class="w"> </span>--<span class="w"> </span>--run npm<span class="w"> </span>run<span class="w"> </span>type-check npm<span class="w"> </span>run<span class="w"> </span>lint npm<span class="w"> </span>run<span class="w"> </span>build </code></pre></div> Expected: all pass (the one known pre-existing unrelated <code>MetadataErrorAlert</code> flake aside); build succeeds.</p> <ul> <li>[ ] <strong>Step 6: Commit</strong></li> </ul> <div class="highlight"><pre><span></span><code>git<span class="w"> </span>add<span class="w"> </span>src/components/tools/LookupTool.tsx<span class="w"> </span>src/components/tools/LookupTool.test.tsx git<span class="w"> </span>commit<span class="w"> </span>-m<span class="w"> </span><span class="s2">"feat(packs): Add to Editor on Lookup (single word) (PHON-170)"</span> </code></pre></div> <hr /> <h2 id="self-review-notes-coverage-vs-spec-57-15">Self-Review notes (coverage vs spec §5.7 / §15)<a class="headerlink" href="#self-review-notes-coverage-vs-spec-57-15" title="Permanent link">¶</a></h2> <ul> <li><strong>Uniform "Add to Editor" across grid tools:</strong> Task 4 (one <code>SelectionToolbar</code> integration → Word Lists, Contrast Sets, Sentences). ✓</li> <li><strong>Single Lookup word:</strong> Task 5. ✓</li> <li><strong>Pack-picker (saved packs + New pack…):</strong> Task 3 <code>AddToEditorButton</code> menu. ✓ (your amendment ask.)</li> <li><strong>Words link to the lexicon (picture cards):</strong> Task 2 maps <code>image_file</code>/<code>image_provider</code> onto <code>PackWord</code>; the editor-cards phase renders them. ✓</li> <li><strong>Confirmation toast with Undo:</strong> Task 3 Snackbar + <code>removeItems</code>. ✓</li> <li><strong>Pack indicator updates:</strong> the chosen pack becomes active; the existing <code>PackIndicatorChip</code> reflects it (no change needed). ✓</li> <li><strong>Deferred / out of scope:</strong> Text-Analysis "add extraction" (marginal fit + would need async word resolution — a follow-up); Speech Analysis (audio stays outside the pack MVP); navigating to the editor on add (the pack indicator is the path — intentional, tool keeps its UX); adding to a NON-active pack without activating it (picking activates — simpler, one active-pack model).</li> </ul> <h2 id="decisions-resolved-at-planning">Decisions resolved at planning<a class="headerlink" href="#decisions-resolved-at-planning" title="Permanent link">¶</a></h2> <ol> <li><strong><code>SelectionToolbar</code> is the single anchor</strong> for the three grid tools — it already carries <code>data</code>/<code>dataType</code>/<code>selectedIndices</code>. One integration, not three.</li> <li><strong>Add the selected rows</strong> (the toolbar is selection-scoped); "Select all" covers "add everything."</li> <li><strong>Picking a pack activates it</strong> (<code>loadPack</code>/<code>newPack</code>) then adds — one active-pack model; the indicator reflects it; no separate add-to-inactive path.</li> <li><strong>Undo via <code>removeItems(addedIds)</code></strong> on the active pack (immediate, within the snackbar window).</li> <li><strong>Text Analysis deferred</strong> (weak fit + async resolution); Lookup included (clear value).</li> </ol> </article> </div> <script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script> </div> <button type="button" class="md-top md-icon" data-md-component="top" hidden> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg> Back to top </button> </main> <footer class="md-footer"> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class="md-copyright"> Made with <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> Material for MkDocs </a> </div> </div> </div> </footer> </div> <div class="md-dialog" data-md-component="dialog"> <div class="md-dialog__inner md-typeset"></div> </div> <script id="__config" type="application/json">{"annotate": null, "base": "../../..", "features": ["navigation.instant", "navigation.tracking", "navigation.tabs", "navigation.sections", "navigation.expand", "navigation.top", "search.suggest", "search.highlight", "content.code.copy"], "search": "../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script> <script src="../../../assets/javascripts/bundle.d7400e89.min.js"></script> </body> </html>