From 93fb69d1de2a8f733cf4352ab2418dda2b00412f Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 13 Mar 2026 01:30:43 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20final=20spec=20fixes=20=E2=80=94=20reso?= =?UTF-8?q?lve=20inputValue=20ownership,=20spinner=20mode,=20clear-search?= =?UTF-8?q?=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...26-03-13-script-generator-phase2-design.md | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md b/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md index 0008ee63..0b1a89b4 100644 --- a/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md +++ b/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md @@ -249,9 +249,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx - Renders an "All" tab first (calls `setCategory(null)`, active when `activeCategoryId === null`), followed by one tab per category - Category tabs: pill style, `bg-primary/10` + left 3px cyan accent bar on active tab -- Search: local `inputValue` state controls the `` value (NOT `searchQuery` from store — avoids input lag during debounce). `useEffect` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup -- Reads `categories`, `activeCategoryId` from store; does NOT use `searchQuery` from store to control the input -- Exposes a `onClearSearch` callback (or reads a `clearSearch` prop) — see `ScriptTemplateList` below for why. Simplest implementation: `ScriptLibraryPage` passes a `clearSearch` callback to both components; `ScriptFilterBar` exposes it as a function ref or the page wires `setInputValue` via a `useRef` +- Receives `inputValue: string` and `setInputValue: (v: string) => void` as props from `ScriptLibraryPage` (state is lifted — `ScriptFilterBar` does NOT own local search state) +- `` is controlled by `inputValue` prop. `useEffect` inside `ScriptFilterBar` watches `inputValue`, schedules `setTimeout(() => setSearch(inputValue), 300)`, clears timeout on cleanup — debounce lives here but the value lives in the page +- Reads `categories`, `activeCategoryId` from store **`ScriptTemplateList`** @@ -260,7 +260,7 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx - Accepts `inputValue: string` and `onClearSearch: () => void` props from `ScriptLibraryPage` - Shows 3 skeleton placeholder cards while `isLoadingTemplates` is true - Shows "No templates found" empty state when `templates.length === 0` and `!isLoadingTemplates` and `inputValue === ''` -- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` which resets both `inputValue` in `ScriptFilterBar` and `searchQuery` in the store. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection +- Shows "No templates match your search" + "Clear search" button when `templates.length === 0` and `!isLoadingTemplates` and `inputValue !== ''`. "Clear search" calls `onClearSearch()` — a callback prop from `ScriptLibraryPage` defined as `() => { setInputValue(''); store.setSearch(''); }`. Using `inputValue` (not `store.searchQuery`) avoids the 300ms debounce lag in empty-state detection **`TemplateCard`** @@ -274,13 +274,14 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx **`ScriptGeneratorPanel`** - Shows placeholder (Terminal icon + "Select a template to get started") when `selectedTemplate` is null -- Shows spinner overlay when `isLoadingDetail` is true (template list remains visible behind it) -- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar -- Action bar (visible when template selected): - - Generate button (`bg-gradient-brand`) — calls `generate()`; shows spinner + disabled while `isGenerating` +- Shows a full-panel centered spinner when `isLoadingDetail` is true, replacing the panel content (not an overlay — no prior template content shown while loading). The template list column remains fully visible and interactive +- When template selected and `!isLoadingDetail`: renders template name/description header, `ScriptParameterForm`, `ScriptPreview`, action bar (in that order, top to bottom) +- Visual order within the panel (top to bottom): warnings callout → `ScriptPreview` → action bar → error text +- `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0` +- Action bar (below preview): + - Generate button (`bg-gradient-brand`) — calls `generate()` with no arguments (Phase 3 will pass `sessionId`); shows spinner + disabled while `isGenerating` - Download `.ps1` button — disabled when `generatedScript` is null; on click triggers `new Blob([generatedScript], { type: 'text/plain' })` → programmatic anchor click with `download="${selectedTemplate.slug}.ps1"` - `generateError` shown as rose-500 text inline below action bar -- `generationWarnings` shown as amber-400 callout above the preview when `generationWarnings.length > 0` - Permission check: call `usePermissions()` directly in this component; derive `canGenerate = !isViewer`. Pass `canGenerate` as a prop down to `ScriptParameterForm`. Generate and Download buttons disabled with tooltip "Engineer access required" when `!canGenerate` **`ScriptParameterForm`** @@ -310,9 +311,10 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx - Two modes determined by `generatedScript` in store: - **Draft mode** (`generatedScript === null`): client-side `{{key}}` substitution in `selectedTemplate.script_body`. Sensitive params (`parameter.sensitive === true`) rendered as `****` regardless of `paramValues` value. Unfilled non-sensitive params left as `{{key}}` for `PowerShellHighlighter` to color amber - **Generated mode** (`generatedScript !== null`): shows `generatedScript` +- The component computes `displayScript` as a local variable (the substituted preview string in draft mode, or `generatedScript` in generated mode). Both the render and the copy handler reference this same local variable — no separate store field needed - Copy icon (Lucide `Copy`, 14px) in top-right corner of code block, visible in both modes: - - On click: copies the currently displayed script string (preview in draft mode, generated script in generated mode) via `navigator.clipboard.writeText()`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state -- Passes current script string to `PowerShellHighlighter` + - On click: calls `navigator.clipboard.writeText(displayScript)`. On success: shows "Copied!" for 2s then resets. On failure: silently fails — no error displayed, icon does not change state +- Passes `displayScript` to `PowerShellHighlighter` **`PowerShellHighlighter`** @@ -330,6 +332,8 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx 7. Keywords: /\b(if|else|elseif|foreach|for|while|function|return|try|catch|finally|param|switch)\b/ ``` + Note: variables (priority 4) consume tokens like `$foreach` before keywords (priority 7) can match — this is intentional. Do not reorder the alternation without reviewing this interaction. + Token colors: - Comments → `text-[#8b949e]` - String literals → `text-[#a5d6ff]` @@ -358,9 +362,9 @@ ScriptLibraryPage pages/ScriptLibraryPage.tsx | ------ | ------------ | | View Script Library page | Any authenticated user | | Browse templates, see draft preview | Any authenticated user | -| Fill form, generate, copy, download | Engineer, owner, or super_admin (i.e. `!isViewer`) | +| Fill form, generate, copy, download | Engineer, owner, or super_admin | -`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (which returns `true` for engineer, owner, and super_admin via the role hierarchy — matching "Engineer or above" intent more explicitly than `!isViewer`). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly. +`usePermissions()` is called once in `ScriptGeneratorPanel`. Derive `canGenerate = isEngineer` (`usePermissions().isEngineer` returns `true` for engineer, owner, and super_admin via the role hierarchy check — equivalent to `!isViewer` given the four-role system, but `isEngineer` is more semantically explicit). Pass `canGenerate` as a prop down to `ScriptParameterForm`. Leaf components (`ScriptParameterField`) receive `disabled` as a prop — they do not call `usePermissions()` directly. Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — known limitation, documented in Architecture section). @@ -374,7 +378,7 @@ Viewer-blocking is frontend-only in Phase 2 (backend role guard deferred — kno - 300ms debounce in `ScriptFilterBar`: local `inputValue` state, `useEffect` + `setTimeout`/`clearTimeout` - `setSearch` is called once after the debounce delay; it immediately calls `loadTemplates()` - Category filter and search compose: `loadTemplates()` sends both `category_slug` and `search` simultaneously -- `inputValue` is lifted to `ScriptLibraryPage` (as state) and passed as a prop to both `ScriptFilterBar` (controls the input) and `ScriptTemplateList` (drives empty-state variant). `onClearSearch` callback (also from `ScriptLibraryPage`) resets `inputValue` to `''` and calls `setSearch('')` +- `inputValue` is owned by `ScriptLibraryPage` (lifted state). `ScriptLibraryPage` passes it as a prop to `ScriptFilterBar` (which controls the ``) and to `ScriptTemplateList` (which uses it to choose the correct empty-state variant). `onClearSearch` — also defined in `ScriptLibraryPage` as `() => { setInputValue(''); store.setSearch(''); }` — is passed to `ScriptTemplateList` for the "Clear search" button ---