Migrates legacy imperative DOM construction to local declarative Lit-html templates rendered inside existing containers.
70%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/ui-eng-vision-local-lit-renderer/SKILL.mdThis subskill converts consolidated imperative DOM construction helper methods into reactive, declarative Lit-html templates, rendering them locally inside the existing element containers.
Conserve Abstraction Topology:
linkifyIcon, maybeCreateLinkToElementsPanel) and class helper
methods in their original locations, converting DOM-returning helpers in
place to return LitTemplate (or Promise<LitTemplate|null>) rather
than deleting and inlining them into performUpdate() or DEFAULT_VIEW.Determine Render Lifecycle per Layer:
performUpdate() to execute
the main lit render call, and use this.requestUpdate() to queue
updates.update() or scheduleUpdate()) and invoke render() directly inside
that method. Do not introduce Widget lifecycle methods
(performUpdate) on classes that do not inherit them.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 Lit or reference
Lit.nothing / Lit.render. Named imports enable better tree-shaking and
maintain consistent codebase style.
Modern Component Mapping:
devtools-toolbar). Do not assume standard Lit defaults.Local Modular Renders:
appendField or createChild on 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.Resolve TypeScript Type Friction:
Element. Lit's render()
function expects HTMLElement | DocumentFragment.as HTMLElement or update its class property type definition from Element
to HTMLElement to satisfy the TypeScript compiler.render(this.template(), this.containerField as HTMLElement);Delegate to Unmigrated Rendering Engines:
HTMLElement or Element instances directly. Treat unmigrated utilities as "black boxes" that generate DOM, and embed their output in standard template expressions: htmlHandle Asynchronous DOM Updates:
.then() callback), render the parent container or a placeholder
template first. Once the data is retrieved, execute a local render()
inside the callback targeting the specific sub-container.Template Factorization Strategies:
nothing: Use Lit's nothing sentinel instead of empty
strings '' or empty templates for conditional rendering.LitTemplate. This avoids
monoliths, preserves readability, and reuses template cache strategies.Visual Parity, CSS Adaptation, and Accessibility:
DataGrid with <devtools-data-grid>), inspect and update the associated .css file. Replace legacy class selectors (.data-grid) with tag selectors (devtools-data-grid).span to
div) or drop class names during template translation, as CSS may
depend on them.<devtools-button>) defaults may not match legacy appearance.
Explicitly bind .data=${{variant: Buttons.Button.Variant.OUTLINED}}
(or appropriate variant) rather than relying on defaults.ARIAUtils.markAsAlert()) to native ARIA
attributes in the template (e.g., role="alert", aria-live="polite").Render Templates Syntax & Code Movement:
// clang-format off and // clang-format on to prevent clang-format from
corrupting template indentation.Composing Child UI.Widgets & Pre-Existing Instances:
${widget(ChildWidgetClass, {params})} Over <devtools-widget>:
Prefer ${widget(ChildWidgetClass, {params})} directly as a child
expression instead of <devtools-widget ${widget(ChildWidgetClass, {params})}></devtools-widget>.
Only use an explicit <devtools-widget> tag when you need to set other
HTML attributes/classes, apply additional directives (such as widgetRef
or ref), or provide child nodes.widget(ChildWidgetClass, {params}):
When instantiating child widgets in a Lit template, strongly prefer the
two-argument form widget(ChildWidgetClass, {propA: input.a, propB: input.b})
over the single-argument factory widget(element => new ChildWidgetClass(element)).
The two-argument form automatically propagates updated parameters and
schedules requestUpdate() on re-renders, whereas a factory callback only
runs once on initial creation. If the child widget currently uses
setFoo() methods instead of setters, convert them into set foo()
setters on the child widget so you can use the two-argument widget()
directive.ViewOutput: Do not store child widget
references on ViewOutput (e.g., output.childWidget = w) as a final
result (only acceptable temporarily in intermediate CLs). Supply all
configuration through setters on the child widget, or if imperative
actions are strictly required, expose individual functions on ViewOutput
(e.g., output.doLayout = () => w.doLayout()).UI.Widget.WrapperWidget: Do NOT instantiate child widgets
or layout containers (new ChildWidget(), UI.TabbedPane.TabbedPane /
tabbedLocation.tabbedPane(), StackLocation.widget(),
UI.ListWidget.ListWidget, UI.SplitWidget.SplitWidget) in the
constructor/presenter and pass them via ViewInput to
UI.Widget.WrapperWidget.
element?: HTMLElement in its constructor and setters for its state,
and instantiate it declaratively with ${widget(ChildWidget, {...})}.TabbedPane and StackLocation: Create tabbed panes and stack
panes declaratively in the view using <devtools-tabbed-pane> (or
${widget(UI.TabbedPane.TabbedPane, ...)}) and
<devtools-stack-pane> instead of wrapping a TabbedPane or
StackLocation.widget() in WrapperWidget.UI.ListWidget.ListWidget + UI.ListWidget.Delegate: Do
NOT keep this.list = new UI.ListWidget.ListWidget(this) and implements UI.ListWidget.Delegate on the presenter, convert renderItem /
beginEdit to render into a DocumentFragment, and pass list via
ViewInput to WrapperWidget. Instead:
ListWidget + ListWidget.Delegate with
<devtools-list> (ui/components/lists/lists.js), passing pure data
(items, editingIndex, validationError) and callbacks in
ViewInput, and handling editing via <devtools-prompt>, an inline
editor row, or UI.Dialog.Dialog.ListWidget.Editor screenshot
goldens temporarily): Move ListWidget + ListWidget.Delegate
ownership out of the presenter into a view-layer child class
extending UI.ListWidget.ListWidget directly and instantiated via
${widget(...)} in DEFAULT_VIEW (with an imperative trigger on
ViewOutput if needed, and without WrapperWidget), keeping
ViewInput pure data and callbacks.WrapperWidget is only justified when reparenting a shared stateful
singleton across multiple containers (such as ConsoleView.instance()
or SourcesView) or bridging an opaque UI.Widget returned by an
external factory. Note that WrapperWidget sets display: contents on
its <devtools-widget> host, so never put sizing, border, or background
classes on the <devtools-widget> hosting WrapperWidget (wrap it in a
<div> instead).Declarative <devtools-split-view> Guidelines:
<devtools-split-view> has flex: auto; height: 100%;
in CSS so it fills available container space and doesn't collapse to 0x0.showMode: When binding show=${...}
declaratively on <devtools-split-view name="...">, reuse the existing
SplitWidget setting ({vertical?: {size, showMode}, horizontal?: {size, showMode}})
for showMode rather than creating new boolean settings (which loses user
preferences and requires KnownContextValues.ts updates).!input.isTraceApp) in the DEFAULT_VIEW template (e.g.,
show=${!input.isTraceApp && input.isSidebarOpen ? 'both' : 'main'})
rather than baking them into the presenter's setting state.resizer-widget classes in
templates; <devtools-split-view> manages its own built-in splitter
unless custom elements are explicitly registered with installResizer.Declarative Long-Press Action Buttons (bindToAction):
UI.Toolbar.Toolbar.createLongPressActionButton(action, [subButtons])
maps declaratively to nesting option <devtools-button> elements directly
inside <devtools-button ${bindToAction('action-id')}>...</devtools-button>.Audit Removed UIStrings:
UIStrings entry 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.Micro-Commit Strategy:
Wait for confirmation:
Before reporting back or committing, re-read the instructions and verify:
{html, nothing, render} from ui/lit/lit.js (and no import * as Lit)?span to div (or vice versa)?aria-*, role, or custom data attributes?// clang-format off?<devtools-split-view> have flex: auto; height: 100%; CSS and reuse existing split settings?UIStrings weren't from accidentally dropped UI branches?ChromeDevTools/devtools-frontend
82e99f9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.