docs: fix spec issues from round 2 review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-13 11:55:19 -04:00
parent e7a3ed6982
commit 0854bca13e

View File

@@ -7,14 +7,27 @@
## Overview
The Script Library left pane gains two distinct modes. In **Browse mode** the user sees the full template list with filter bar and a "Configure →" button on each card. Clicking "Configure →" transitions the entire left pane to **Configure mode**, which replaces the list with a full-height view of the selected template's header, parameter form, and action buttons. The right pane (preview/output) is always visible. "Back to library" returns to Browse mode with filter/search state preserved.
The Script Library left pane gains two distinct modes. In **Browse mode** the user sees the full template list with filter bar and a "Configure →" button on each card. Clicking "Configure →" transitions the entire left pane (including the filter bar) to **Configure mode**, which replaces the list with a full-height view of the selected template's header, parameter form, and action buttons (Generate, Download, Copy). The right pane becomes a **read-only preview** (`ScriptPreview` only — no form or buttons). "Back to library" returns to Browse mode with filter/search state preserved.
---
## Structural Change Summary
This is a cross-column relocation of the Generate/Download/Copy controls:
| Before | After |
|--------|-------|
| Left pane: template list + filter bar | Left pane: Browse mode (list + filter) OR Configure mode (form + actions) |
| Right pane: param form + action buttons + preview | Right pane: `ScriptPreview` only (read-only display) |
`ScriptGeneratorPanel` (which currently owns the right column's form, actions, and preview) is **deleted**. The right pane becomes `ScriptPreview` in isolation. The new `ScriptConfigurePane` component owns the left pane in Configure mode and contains the form + all action buttons.
---
## Goals
- Make template selection intentional (no accidental click-to-configure)
- Give the parameter form more vertical space
- Give the parameter form more vertical space by using the full left pane height
- Keep the output preview always visible on the right
- Preserve filter/search state across Browse ↔ Configure transitions
@@ -22,62 +35,108 @@ The Script Library left pane gains two distinct modes. In **Browse mode** the us
## Non-Goals
- No changes to the right pane (`ScriptPreview`, `ScriptGeneratorPanel` output section)
- No changes to `ScriptPreview` internals — it moves to the right pane as-is, including its existing copy overlay button
- No changes to the Zustand store shape or actions
- No changes to the filter/search debounce logic
- No changes to routing
---
## New Work (not pre-existing)
The **Copy button in the action bar** is new — it does not exist in the current `ScriptGeneratorPanel`. It must be implemented as a new button in `ScriptConfigurePane` that copies `generatedScript` (or `displayScript` from `ScriptPreview`'s computation). Since `ScriptPreview` owns the substitution logic, the simplest approach is for the action-bar Copy to copy `generatedScript` from the store when available, and fall back to the draft preview via a shared utility or by co-locating the substitution logic. Implementation note: the action-bar Copy can duplicate `ScriptPreview`'s `handleCopy` behavior using `store.generatedScript` for the generated state; the draft substitution case is handled by `ScriptPreview`'s own overlay copy button.
---
## Left Pane — Two Modes
### Browse Mode
Rendered when `selectedTemplate === null` OR when the user has returned via "Back to library".
Rendered when `paneMode === 'browse'`.
Layout (top to bottom, full pane height):
The page header (`h1` "Script Library" + subtitle) stays at the top of `ScriptLibraryPage` above the two-column grid, visible in both pane modes — it is not affected by this change.
1. **Filter bar** (`ScriptFilterBar`) — category pills + search input, same as current
The current `ScriptLibraryPage` renders `ScriptFilterBar` at the page level above the two-column grid. In this redesign, the filter bar moves **inside the left pane column** so it can be hidden in Configure mode. `inputValue`/`setInputValue` remain owned by `ScriptLibraryPage` (not inside the left pane sub-tree) so the search text survives the unmount when the pane switches to Configure mode.
Layout (top to bottom, fills left pane height):
1. **Filter bar** (`ScriptFilterBar`) — category pills + search input
2. **Template list** (`ScriptTemplateList`) — scrollable, fills remaining height
**TemplateCard changes:**
- Remove the full-card `onClick` that calls `selectTemplate()`
- Remove the active/selected visual state (`bg-primary/10 border-l-[3px]` etc.) — cards are never "selected" in browse mode, they are only "configured"
- Add a **"Configure →"** button to each card (right-aligned, replaces the old click-anywhere behavior)
- Root element changes from `<button>` to `<div>` — the card itself is no longer clickable
- Remove active/selected visual state (`bg-primary/10 border-l-[3px]` etc.) — no card is "selected" in browse mode
- Add **"Configure →"** button, right-aligned in the bottom row
- Style: `bg-primary/10 border border-primary/20 text-primary text-xs px-2.5 py-1 rounded-md hover:bg-primary/20 transition-colors`
- On click: calls `selectTemplate(template.id)` then transitions pane to Configure mode
- Uses unicode `→` (not a Lucide icon)
- On click: calls `onConfigure(template.id)` prop
- The rest of the card (name, description, tags, complexity badge) is non-interactive
**Updated `TemplateCard` props:**
```tsx
interface Props {
template: ScriptTemplateListItem
onConfigure: (id: string) => void
}
```
`TemplateCard` also removes its `useScriptGeneratorStore` import entirely — it no longer reads `selectedTemplate` or `selectTemplate` from the store.
### Configure Mode
Rendered when `selectedTemplate !== null` (or `isLoadingDetail === true`).
Rendered when `paneMode === 'configure'`.
The entire left pane (including filter bar area) is replaced by the Configure view. No filter bar visible.
The entire left pane (including filter bar) is replaced by the Configure view.
Layout (top to bottom, full pane height, `overflow-y-auto`):
1. **Back button** — top of pane
- `← Back to library`
1. **Back button**
- Label: `← Back to library`
- Style: `flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4`
- On click: calls `store.clearOutput()` then sets pane mode back to Browse (see State section)
- Does NOT call `selectTemplate(null)` or clear `selectedTemplate` in the store — the store selection is preserved; only the pane view changes
- On click: calls `onBack` prop → `ScriptLibraryPage` calls `store.clearOutput()` then `setPaneMode('browse')`
- Does NOT clear `selectedTemplate` in the store (no such action exists — `selectTemplate` only accepts a string ID). `selectedTemplate` remains set in the store after returning to Browse mode. The right pane (`ScriptPreview`) continues showing the last preview while browsing — this is intentional.
2. **Template header**
2. **Template header** (visible when `isLoadingDetail === false`)
- Name: `text-base font-semibold font-heading text-foreground`
- Description (if present): `text-sm text-muted-foreground mt-0.5`
- Tag row: complexity badge + category name + template tags (first 3, overflow as `+N`) — same badge styles as current `TemplateCard`
- `ShieldAlert` elevation icon if `requires_elevation`
- Tag row (left-to-right): `ShieldAlert` icon if `requires_elevation`, complexity badge, category name (resolved from `store.categories.find(c => c.id === selectedTemplate.category_id)?.name`), template tags (first 3, overflow as `+N`) — same badge/tag styles as current `TemplateCard`. If no matching category is found, omit the category name chip.
- Separator: `border-t border-border mt-3 pt-3`
3. **`ScriptParameterForm`** (existing component, `canGenerate` prop unchanged)
3. **`ScriptParameterForm`** existing component, `canGenerate` prop unchanged
4. **Action bar**
- **Generate** button: full-width, `bg-gradient-brand`, same disabled/loading behavior as current
4. **Warnings callout** — shown above Generate when `generationWarnings.length > 0` (same amber box as current `ScriptGeneratorPanel`)
5. **Action bar**
- **Generate** button: full-width, `bg-gradient-brand`, disabled/loading behavior same as current
- **Download .ps1** + **Copy** buttons: side by side below Generate, each `flex-1`
- The Copy button in the action bar copies `generatedScript` (or the draft display if not yet generated). `ScriptPreview`'s existing copy overlay is **retained as-is** — two copy entry-points is acceptable.
- Error text below if `generateError` is set
- Warnings callout above Generate if `generationWarnings.length > 0` (same amber box as current)
**Loading state:** When `isLoadingDetail === true`, the Configure pane shows a centered `<Loader2 size={28} className="text-primary animate-spin" />` instead of the template content. Same as current panel behavior.
**Loading state:** When `isLoadingDetail === true`, shows a centered `<Loader2 size={28} className="text-primary animate-spin" />` filling the pane instead of the template content.
---
## Right Pane
After the redesign, the right pane contains **only `ScriptPreview`**, wrapped in a `glass-card-static h-full` container.
Two sub-states:
- **No template ever selected** (`selectedTemplate === null`): show empty state — Terminal icon + "Select a template to get started" text. **Important:** `ScriptPreview` returns `null` when `selectedTemplate` is null, so the empty state must be rendered by the right-pane wrapper in `ScriptLibraryPage`, not by `ScriptPreview` itself. Pattern:
```tsx
{selectedTemplate === null ? (
<div className="glass-card-static h-full flex flex-col items-center justify-center gap-3 text-center p-8">
<Terminal size={40} className="text-muted-foreground/40" />
<p className="text-sm text-muted-foreground">Select a template to get started</p>
</div>
) : (
<div className="glass-card-static h-full overflow-y-auto p-4">
<ScriptPreview />
</div>
)}
```
- **Template selected** (in either pane mode): show `ScriptPreview`
The right pane is always read-only — no form, no Generate/Download buttons.
---
@@ -90,54 +149,68 @@ const [paneMode, setPaneMode] = useState<'browse' | 'configure'>('browse')
```
Transitions:
- `'browse' → 'configure'`: triggered by "Configure →" button click (after `selectTemplate()` call)
- `'configure' → 'browse'`: triggered by "Back to library" button click
**Filter/search preservation:** Filter bar state (`inputValue`, `setInputValue`, store's `activeCategoryId`, `searchQuery`) is untouched by pane mode transitions. The filter bar simply unmounts in configure mode and remounts with the same state in browse mode.
- `'browse' → 'configure'`: `onConfigure(id)` — calls `store.selectTemplate(id)` then `setPaneMode('configure')`
- `'configure' → 'browse'`: `onBack()` — calls `store.clearOutput()` then `setPaneMode('browse')`
**`isLoadingDetail` drives configure pane content, not pane mode.** When the user clicks "Configure →":
1. `selectTemplate(id)` is called (sets `isLoadingDetail: true` in store)
2. `setPaneMode('configure')` is called immediately — the user sees the loading spinner in configure mode, not a flash of browse mode
3. When `isLoadingDetail` becomes false, the full configure view renders
2. `setPaneMode('configure')` is called immediately — user sees the loading spinner in configure mode
**`selectTemplate` failure case:** If `selectTemplate` throws (network error), the store resets `isLoadingDetail: false` but leaves `selectedTemplate` unchanged. The pane mode remains `'configure'`. If this is the first selection (`selectedTemplate === null`), the configure pane will show neither a spinner nor template content — it will render nothing. `ScriptConfigurePane` must handle this: if `!isLoadingDetail && !selectedTemplate`, render an error state ("Failed to load template. ← Back to library").
**Filter/search preservation:** `inputValue` remains owned by `ScriptLibraryPage` at the page level. The filter bar unmounts in configure mode and remounts in browse mode with the same `inputValue`. Store's `activeCategoryId` and `searchQuery` are never cleared by pane transitions.
---
## Component Changes
### `ScriptLibraryPage.tsx` — Modified
| File | Change |
|------|--------|
| `frontend/src/pages/ScriptLibraryPage.tsx` | Add `paneMode` state; add `usePermissions` import for `canGenerate`; move `ScriptFilterBar` into left pane column; add `onConfigure`/`onBack` callbacks; render `ScriptConfigurePane` or browse content in left pane conditionally; render right pane as `ScriptPreview`-only with empty state |
| `frontend/src/components/scripts/TemplateCard.tsx` | Root `<button>` → `<div>`; remove `onClick`/active-state; add `onConfigure: (id: string) => void` prop; add "Configure →" button |
| `frontend/src/components/scripts/ScriptTemplateList.tsx` | Accept `onConfigure: (id: string) => void` prop; pass to each `TemplateCard` |
| `frontend/src/components/scripts/ScriptConfigurePane.tsx` | **New** — configure mode layout (back button, template header, `ScriptParameterForm`, warnings, action bar with Generate + Download + Copy) |
| `frontend/src/components/scripts/ScriptGeneratorPanel.tsx` | **Delete** — superseded by `ScriptConfigurePane` and right-pane simplification |
- Add `paneMode` state (`'browse' | 'configure'`)
- Pass `onConfigure` callback to `ScriptTemplateList` (called with `template.id` on "Configure →" click)
- `onConfigure` calls `selectTemplate(id)` then `setPaneMode('configure')`
- Pass `onBack` callback to the configure pane
- `onBack` calls `clearOutput()` then `setPaneMode('browse')`
- Render left pane conditionally:
- `paneMode === 'browse'`: `ScriptFilterBar` + `ScriptTemplateList`
- `paneMode === 'configure'`: new `ScriptConfigurePane` component
No store changes. No API changes. No routing changes.
### `TemplateCard.tsx` — Modified
---
- Remove `onClick` on the card root button; change root element to `<div>`
- Remove `isActive` / active border-l styling
- Add "Configure →" button
## Visual Spec
### `ScriptTemplateList.tsx` — Modified
### Page layout (Configure mode active)
- Accept `onConfigure: (id: string) => void` prop
- Pass it to each `TemplateCard`
```
┌─ left pane (320px) ────────────┬─ right pane (1fr) ──────────────────┐
│ ← Back to library │ │
│ │ [ScriptPreview — always visible] │
│ Restart Windows Service │ │
│ Stops and restarts a service │ # Restart Windows Service │
│ 🛡 [Beginner] [Services] [win] │ param( │
│ ───────────────────────────── │ $ServiceName = "{{service_name}}" │
│ Service Name * │ ) [copy overlay] │
│ [________________] │ │
│ Target Computer │ │
│ [localhost______] │ │
│ Verify after restart [✓] │ │
│ │ │
│ [ Generate Script ] │ │
│ [ Download .ps1 ] [ Copy ] │ │
└────────────────────────────────┴──────────────────────────────────────┘
```
### `ScriptConfigurePane.tsx` — New Component
### TemplateCard — Browse mode
Encapsulates the configure mode layout:
- Back button → calls `onBack` prop
- Template header (name, description, tags, complexity, elevation)
- Loading spinner when `isLoadingDetail`
- `ScriptParameterForm` (with `canGenerate`)
- Warnings callout
- Action bar: Generate (full-width), Download .ps1 + Copy (side-by-side below)
- Error text
```
┌──────────────────────────────────────────────────────┐
│ Restart Windows Service 🛡 [Beginner] │
│ Stops and restarts a named service │
│ 4× used [services] [windows] +1 [Configure →] │
└──────────────────────────────────────────────────────┘
```
### `ScriptConfigurePane` props
Props:
```tsx
interface Props {
canGenerate: boolean
@@ -145,46 +218,4 @@ interface Props {
}
```
All data read directly from Zustand store (`selectedTemplate`, `isLoadingDetail`, `generatedScript`, etc.).
### `ScriptGeneratorPanel.tsx` — Removed (or repurposed)
The current `ScriptGeneratorPanel` is superseded by `ScriptConfigurePane`. It should be deleted; its logic is absorbed into `ScriptConfigurePane`.
---
## Visual Spec
### Configure Pane — action bar
```
┌─────────────────────────────────┐
│ [Generate Script] │ ← full-width, bg-gradient-brand
│ [Download .ps1] [Copy] │ ← side-by-side, flex-1 each
│ <error text if any> │
└─────────────────────────────────┘
```
### "Configure →" button on TemplateCard
```
┌──────────────────────────────────────────────────┐
│ Restart Windows Service [Beginner] 🛡 │
│ Stops and restarts a named service │
│ services windows +1 [Configure →]│
└──────────────────────────────────────────────────┘
```
---
## Affected Files
| File | Change |
|------|--------|
| `frontend/src/pages/ScriptLibraryPage.tsx` | Add `paneMode` state, `onConfigure`/`onBack` callbacks, conditional left-pane render |
| `frontend/src/components/scripts/TemplateCard.tsx` | Remove full-card click, add "Configure →" button |
| `frontend/src/components/scripts/ScriptTemplateList.tsx` | Accept + thread `onConfigure` prop |
| `frontend/src/components/scripts/ScriptConfigurePane.tsx` | New — configure mode layout |
| `frontend/src/components/scripts/ScriptGeneratorPanel.tsx` | Delete — superseded by `ScriptConfigurePane` |
No store changes. No API changes. No routing changes.
All other data read from Zustand store directly.