Subskill: Local Lit-HTML Renderer (Pass 2)
This subskill converts consolidated imperative DOM construction helper methods into reactive, declarative Lit-html templates, rendering them locally inside the existing element containers.
1. Declarative Technology Migration
-
Conserve Abstraction Topology:
- Maintain the existing class hierarchy, helper class boundaries, and logical groups.
- Preserve existing standalone/module-level DOM helper functions (e.g.,
linkifyIcon,maybeCreateLinkToElementsPanel) and class helper methods in their original locations, converting DOM-returning helpers in place to returnLitTemplate(orPromise<LitTemplate|null>) rather than deleting and inlining them intoperformUpdate()orDEFAULT_VIEW. - Do not aggressively "flatten" classes or collapse helper abstractions into the main view just to coerce a single master template. Pass 2 should only change the implementation details of rendering (imperative -> declarative) within existing compartments, not the public API or responsibilities of objects.
-
Determine Render Lifecycle per Layer:
- For UI.Widget classes: Define or update
performUpdate()to execute the main litrendercall, and usethis.requestUpdate()to queue updates. - For Delegate/Helper classes: Identify the custom update hook (e.g.,
update()orscheduleUpdate()) and invokerender()directly inside that method. Do not introduce Widget lifecycle methods (performUpdate) on classes that do not inherit them.
- For UI.Widget classes: Define or update
-
Import Modern Templating (Named Imports Only):
-
Always use named imports from
../../ui/lit/lit.js:import {html, nothing, render, Directives, type LitTemplate} from '../../ui/lit/lit.js'; -
NEVER use namespace imports like
import * as Litor referenceLit.nothing/Lit.render. Named imports enable better tree-shaking and maintain consistent codebase style.
-
-
Modern Component Mapping:
- MANDATORY: You MUST read and load ui_engineering.md first to understand specific component mappings (e.g., Toolbar vs
devtools-toolbar). Do not assume standard Lit defaults. - If unsure, also consult the automatic migration code
- Prefer Component Built-Ins Over Legacy Boilerplate: Before porting legacy event handlers, comparators, or state variables, check if the modern component handles them natively. Drop host-level workarounds in favor of declarative component attributes.
- MANDATORY: You MUST read and load ui_engineering.md first to understand specific component mappings (e.g., Toolbar vs
-
Local Modular Renders:
- If the class contains multiple separate legacy container fields (e.g.,
fields generated by
appendFieldorcreateChildon a parent layout), prefer local modular renders (rendering templates individually into each container) to preserve the legacy layout framework. Do not force a monolithic template if doing so violates the existing topology.
- If the class contains multiple separate legacy container fields (e.g.,
fields generated by
-
Resolve TypeScript Type Friction:
- Legacy DevTools elements may be typed as
Element. Lit'srender()function expectsHTMLElement | DocumentFragment. - When rendering into legacy containers, cast the container using
as HTMLElementor update its class property type definition fromElementtoHTMLElementto satisfy the TypeScript compiler. - Example:
render(this.template(), this.containerField as HTMLElement);
- Legacy DevTools elements may be typed as
-
Delegate to Unmigrated Rendering Engines:
- Do not attempt to rewrite complex historical subsystems (like Linkifiers or specialized UI utilities) to be "pure Lit" during this pass.
- Capitalize on Lit's ability to interpolate standard
HTMLElementorElementinstances directly. Treat unmigrated utilities as "black boxes" that generate DOM, and embed their output in standard template expressions:html${this.legacyElement}``. - CRITICAL: Before treating any component as "unmigrated," you MUST follow steps in Section 4. You are only allowed to use this escape hatch if no declarative migration instructions exist.
-
Handle Asynchronous DOM Updates:
- For asynchronous callback-driven updates (e.g., elements updated in a
.then()callback), render the parent container or a placeholder template first. Once the data is retrieved, execute a localrender()inside the callback targeting the specific sub-container.
- For asynchronous callback-driven updates (e.g., elements updated in a
-
Template Factorization Strategies:
- Prefer
nothing: Use Lit'snothingsentinel instead of empty strings''or empty templates for conditional rendering. - Parameterized Fragment Factories: For repeated UI patterns or
intermediate layout branches, extract them into parameterized helper
functions inside the class scope returning
LitTemplate. This avoids monoliths, preserves readability, and reuses template cache strategies.
- Prefer
-
Visual Parity, CSS Adaptation, and Accessibility:
- Zero-Tolerance Regression: Screenshot tests are the ground truth for this phase. Any visual diff (above 0%) in the generated screenshots is unacceptable and must be resolved before proceeding.
- CSS Selector Migration: When replacing legacy imperative components/widgets with modern web components (e.g., replacing legacy
DataGridwith<devtools-data-grid>), inspect and update the associated.cssfile. Replace legacy class selectors (.data-grid) with tag selectors (devtools-data-grid). - Strict Tag/Class Parity: Do not change tag types (e.g.,
spantodiv) or drop class names during template translation, as CSS may depend on them. - Explicit Component Variants: Modern Custom Element (e.g.
<devtools-button>) defaults may not match legacy appearance. Explicitly bind.data=${{variant: Buttons.Button.Variant.OUTLINED}}(or appropriate variant) rather than relying on defaults. - Accessibility Preservation: Convert custom accessibility
instrumentation (like
ARIAUtils.markAsAlert()) to native ARIA attributes in the template (e.g.,role="alert",aria-live="polite").
-
Render Templates Syntax & Code Movement:
- Surround the expressions containing lit template literals with
// clang-format offand// clang-format onto prevent clang-format from corrupting template indentation. - Minimize Code Movement: Avoid moving existing code to a different location in the file. Put the function signature around the existing code to minimize the diff.
- The Gerrit Code Review UI cannot identify moved blocks of unchanged code.
- If blocks of code need to be reordered, create a "prefactoring" change that first extracts the code that needs to be reordered into a named helper functions and pause the migration, waiting user confirmation.
- Surround the expressions containing lit template literals with
-
Composing Child
UI.Widgets & Pre-Existing Instances:- Strongly Prefer Two-Argument
widget(ChildWidgetClass, {params}): When instantiating child widgets in a Lit template, strongly prefer the two-argument formwidget(ChildWidgetClass, {propA: input.a, propB: input.b})over the single-argument factorywidget(element => new ChildWidgetClass(element)). The two-argument form automatically propagates updated parameters and schedulesrequestUpdate()on re-renders, whereas a factory callback only runs once on initial creation. If the child widget currently usessetFoo()methods instead of setters, convert them intoset foo()setters on the child widget so you can use the two-argumentwidget()directive. - Pre-Existing Widget Instances (
UI.Widget.WrapperWidget): Use${widget(UI.Widget.WrapperWidget, {widget: input.childWidget})}when embedding widgets owned byUI.ViewManager(TabbedViewLocation.tabbedPane(),StackLocation.widget()) or when conditionally moving a shared widget instance between containers ({widget: shouldShow ? this.#sharedWidget : null}). WrapperWidgetautomatically handles showing and detaching the inner widget and forwards calculateConstraints without requiring custom wrapper classes.
- Strongly Prefer Two-Argument
-
Declarative
<devtools-split-view>Guidelines:- CSS Sizing: Ensure
<devtools-split-view>hasflex: auto; height: 100%;in CSS so it fills available container space and doesn't collapse to 0x0. - Reuse Existing Settings for
showMode: When bindingshow=${...}declaratively on<devtools-split-view name="...">, reuse the existingSplitWidgetsetting ({vertical?: {size, showMode}, horizontal?: {size, showMode}}) forshowModerather than creating new boolean settings (which loses user preferences and requiresKnownContextValues.tsupdates). - Keep Environment Overrides in Templates: Keep environment overrides
(like
!input.isTraceApp) in theDEFAULT_VIEWtemplate (e.g.,show=${!input.isTraceApp && input.isSidebarOpen ? 'both' : 'main'}) rather than baking them into the presenter's setting state. - Clean Up Resizers: Do not leave unused
resizer-widgetclasses in templates;<devtools-split-view>manages its own built-in splitter unless custom elements are explicitly registered withinstallResizer.
- CSS Sizing: Ensure
-
Declarative Long-Press Action Buttons (
bindToAction):- Legacy
UI.Toolbar.Toolbar.createLongPressActionButton(action, [subButtons])maps declaratively to nesting option<devtools-button>elements directly inside<devtools-button ${bindToAction('action-id')}>...</devtools-button>.
- Legacy
-
Audit Removed
UIStrings:- If a
UIStringsentry becomes unused during template migration, verify that a conditional UI branch (such as a dynamically shown checkbox, button, or tooltip) wasn't accidentally dropped before deleting the string.
- If a
-
Micro-Commit Strategy:
- Do not attempt to convert every helper method and UI component in a single monolithic commit. Break the template extraction into small, self-contained micro-commits (e.g., migrating one helper method, toolbar button, or list item render at a time).
- Verify with build and run tests after each micro-commit to guarantee zero regressions and 100% visual parity before moving to the next compartment.
- Provide the exact list of files and commit message when presenting progress so the change history remains atomic and easy to review.
-
Wait for confirmation:
- Wait for an explicit confirmation from the user (or Parent Orchestrator Agent) before proceeding to the next step.
🔍 Mental Audit (Internal Self-Correction)
Before reporting back or committing, re-read the instructions and verify:
- ❓ Named Imports: Did I use named imports
{html, nothing, render}fromui/lit/lit.js(and noimport * as Lit)? - ❓ Tag Parity: Did I accidentally change any
spantodiv(or vice versa)? - ❓ Attribute Drop: Did I drop any
aria-*,role, or custom data attributes? - ❓ Clang-Off Boundary: Is the template wrapped in
// clang-format off? - ❓ Split Views: Does
<devtools-split-view>haveflex: auto; height: 100%;CSS and reuse existing split settings? - ❓ String Audit: Did I verify that any removed
UIStringsweren't from accidentally dropped UI branches? - ❓ Build: Does the code compile and do visual tests pass with 0% diff?