Specifies the create-overlay modal redesign (field reorder, custom radio-list, switch instead of checkbox, drop legacy path hint) and the workshop-items section restructure (drop input-mode radio in favor of autodetected items-vs-collections via batched GetCollectionDetails). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Create Overlay Modal + Workshop Items Section — Redesign
Context
Two UI surfaces still wear the pre-redesign vocabulary even though the global
stylesheet was reworked from first principles in 308fa4e:
-
Create-overlay modal (
templates/overlays.html,components.css:114-166) — uses the old.modal*class names, a<fieldset>with a native<legend>border around the Type radios (looks notched and archaic), native checkbox markup that wraps the input on its own line above the label, and a field order that buries the most-important field (Name) under the most-cluttered one (Type). The "path is generated automatically" hint at the bottom is stale copy from an earlier version where users picked their own paths; paths are now derived from the internal id, so the hint describes behavior the user can no longer influence. -
Workshop items section on the overlay detail page (
templates/overlay_detail.html:43-67) — same fieldset-border issue on the Items/Collection radios, two right-aligned buttons (Add,Refresh from Steam) overlapping with no margin (visual regression), and a forced choice between "items" and "collection" input modes that exists purely to tell the backend which Steam API endpoint to call. The dual mode is also a silent footgun: a user pasting a collection URL into the items field today produces a broken overlay with no warning.
The goal is to bring both surfaces in line with the redesigned stylesheet and, while we're in there, simplify the workshop form structurally (drop the Input-mode radio entirely — see decision rationale below).
Decisions
Create modal
- Field order: Name → Type → System-wide. Name is the most-typed field and the simplest input; it goes first. Type is the bigger decision but benefits from the user having committed to something before they confront three options.
- No fieldset border. Type label becomes a regular
.field-label(small uppercase or semibold, matches Name's label). - Radios become a stacked custom-styled list. Each row: a circular dot control (outer ring with a colored inner dot when selected), bold label on the right, muted second-line description below the label. All descriptions are visible at once — users can compare options without clicking. Considered and rejected: segmented control — breaks down past 3-4 options (the codebase is plausibly heading toward more overlay types: git, zip, mirror). Considered and rejected: full selectable cards — too much vertical space for what is ultimately a single radio group.
- System-wide checkbox becomes a switch. Switch sits left-aligned in the row (same left edge as the radio dots), with bold label + muted second-line description to the right — visually consistent with the radio rows above. Switch is conceptually distinct from the type radios (binary on/off vs. one-of-three), so the different control shape reinforces the hierarchy.
- Drop the "path is generated automatically" paragraph. Legacy copy.
- Buttons keep their current placement (Cancel + Create, right-aligned in a bordered footer with a slightly muted background) — that part of the current modal already works.
Workshop items section
-
Drop the Input-mode fieldset entirely. Single textarea accepts any mix of item IDs, item URLs, and collection URLs. Backend autodetects.
-
Autodetect strategy (verified against live Steam API during brainstorming):
- Parse pasted input into a list of IDs (existing helper:
steam_workshop.parse_workshop_input()). - One batched call to
ISteamRemoteStorage/GetCollectionDetails/v1/with all parsed IDs. Steam returns one entry per ID:result: 1+children: [...]→ it's a collection, expand to children IDs.result: 9(k_EResultFileNotFound) → not a collection, keep ID as-is.
- One batched call to
ISteamRemoteStorage/GetPublishedFileDetails/v1/with the flat list of final item IDs (collection-children + non-collection IDs). - Persist.
Cost: one extra Steam round-trip on submission (~150 ms), regardless of input size. This is simpler than today's code, which has two separate handler branches in
routes/workshop_routes.py:36-99. The unified flow deletes theif input_mode == "items" / elif input_mode == "collection"branching. - Parse pasted input into a list of IDs (existing helper:
-
"Refresh from Steam" relocates to a controls row below the items table (not inside the table). The table ends with its last data row. Below the table sits a single row containing:
- Left: a summary hint (
{n} items · {total_size} totalor0 itemswhen empty). - Right: a normal-styled
↻ Refresh from Steambutton (disabled when the table is empty).
- Left: a summary hint (
-
Add button placement. Right-aligned in its own row immediately below the textarea, with proper top margin (today's "no margin" overlap with Refresh is the bug being fixed by moving Refresh).
-
Textarea uses monospace font since pasted content is IDs and URLs.
-
Header copy stays as
Workshop items(page has other sections —Files,Used by— and section headers aid scanning). -
Helper text under the label: "Paste Steam Workshop IDs, item URLs, or collection URLs — one per line. Collections expand automatically." The last sentence is load-bearing — it tells the user they don't need to pre-classify their input.
Implementation surface
Files to modify
| Path | Change |
|---|---|
l4d2web/l4d2web/templates/overlays.html |
Reorder fields (Name → Type → System-wide); replace native fieldset+radios+checkbox with new .field / .radio-row / .switch-row markup; drop the path-hint <p>; rename .modal* → .dialog* if aligning with the redesign plan. |
l4d2web/l4d2web/templates/overlay_detail.html:43-67 |
Delete the <fieldset class="workshop-input-mode"> block; keep textarea but rewrite its surrounding markup as a .field block with label + helper text; move Refresh from Steam out of its own <form> and put it in a single .table-actions row below the items table; add a summary span on the left of that row. |
l4d2web/l4d2web/templates/_overlay_item_table.html |
Verify whether per-row actions exist (e.g., remove-item). Out of scope to change today, but flag for the implementation session: the design preserves whatever per-row actions are there. |
l4d2web/l4d2web/static/css/components.css |
Add the new component CSS (see below). Existing .modal* rules either stay (if we keep old class names) or get renamed to .dialog*. Remove .workshop-input-mode (no rules to remove — fieldset class has zero CSS today anyway). |
l4d2web/l4d2web/routes/workshop_routes.py:36-99 |
Delete the input-mode branching; unify the handler to (1) parse input, (2) batch-resolve collections, (3) batch-fetch metadata. Existing helpers parse_workshop_input(), resolve_collection(), fetch_metadata_batch() all get reused. |
l4d2web/l4d2web/steam/steam_workshop.py |
Add (or refactor): a partition_collections_and_items(ids) helper that does one GetCollectionDetails batch call and returns (item_ids, collection_id_to_children). The exact shape can mirror existing module conventions. |
Component CSS to add
These should live in components.css and be reusable beyond just these two
surfaces — they form a small set of primitives the rest of the app can adopt
as it migrates.
.field(existing pattern, may already partially exist) — grid container withgap: var(--space-xs); children are.field-label, optional.field-hint, and the control..radio-row— flex row, gapvar(--space-s), custom radio dot via::after;.radio-row.is-selectedcolors the inner dot..radio-list— grid container for a vertical stack of.radio-rows withgap: var(--space-xs). Replaces the<fieldset>pattern..switch+.switch-row— pill-shaped toggle, on-state uses--color-button-primary. Left-aligned in its row, consistent with.radio-rowleft edge..table-actions— flex row withjustify-content: space-between,align-items: center, top marginvar(--space-m). Sits below a.table-wrap.
Existing tokens (already in tokens.css) are sufficient. No new color or
spacing tokens needed.
Things explicitly not changing
- The
<dialog>open/close JavaScript (data-inline-modal-closehandlers) stays as-is. - The overlay detail page's
FilesandUsed bysections. - The
Delete overlay/Renameactions at the bottom of the page. - The Steam-side caching/refresh logic — only the UI placement of the refresh button is moving.
Verification
End-to-end checks for the implementation session:
- Dev server:
python scripts/dev-server.py(not plainflask run— the latter misroutesLEFT4ME_ROOTon macOS). - Create modal: open the overlays list page, click "Create overlay".
- Verify field order: Name → Type → System-wide.
- Verify no fieldset border around Type.
- Verify custom radio dots fill with accent color on selection.
- Verify switch toggles state and visually animates.
- Verify no "path is generated automatically" copy anywhere.
- Submit with each of the three types in turn; verify the overlay is created with the correct type each time.
- Workshop section:
- Create a workshop overlay; navigate to its detail page.
- Verify only one textarea + one
Addbutton (no input-mode radio). - Paste a single item ID (e.g.
3726529483); click Add. Item appears. - Paste a collection URL (e.g.
https://steamcommunity.com/sharedfiles/filedetails/?id=3724125665); click Add. The 6 children appear, not the collection itself. - Paste a mix of items and a collection in one submission; verify all resolve correctly.
- Verify items table ends at its last row (no internal footer bar).
- Verify summary +
↻ Refresh from Steamsit below the table as a single row. - Click Refresh from Steam; verify metadata refresh fires.
- Stale-content sweep:
grep -rn "path is generated automatically" l4d2web/should return no matches after the change. - i18n check (if applicable): if the project uses i18n strings for these screens, verify the removed/changed strings are cleaned up.
Open follow-ups (out of scope)
- Once the new component CSS (
.radio-row,.switch-row,.field,.table-actions) lands, sweep the rest of the templates for fieldsets and native checkboxes that could adopt the same vocabulary. Don't do this in the same commit — surface it as a follow-up so the diff for this change stays scoped. - The
superpowers:brainstormingskill's companion server has an owner-PID detection bug that kills the server when launched viaBash(run_in_background: true)on macOS. Workaround during this brainstorm was launchingnode server.cjsdirectly withBRAINSTORM_OWNER_PID=1. Small upstream PR opportunity, unrelated to this codebase.