A high-performance, vanilla-web dashboard for managing community icon requests for Lawnicons. It transforms static JSON datasets into an interactive triage tool for contributors to visualize, filter, and bulk-export assets and metadata.
Included:
- Infinite scrolling/Lazy loading for datasets exceeding 10k items.
- Deep-linking via URL parameters for shared filter states.
- Client-side bulk zipping and metadata generation (AppFilter, PR templates).
- Material Design 3 (M3) compliant UI with full keyboard accessibility.
- Local Python scripts for data enrichment and build-time tag generation.
Excluded (non-goals):
- Server-side state or user databases (remain strictly static).
- Framework-specific implementations (React, Vue, etc.).
- Direct file system writes from the browser.
- Languages: JavaScript (ESNext), CSS3 (Modern), HTML5, Python 3.10+ (scripts).
- Frameworks: None (Vanilla).
- Runtime: Browser-based.
- Build system: Static Deployment (Vercel).
- Data Layer:
requests.jsonplus auxiliary analytics/filter datasets (sets_stats.json,creation_odds.json,domain_stats.json,activity_stats.json, andassets/filters/*.json). - State Layer: Runtime state is centralized in
App.statewith lookup maps for IDs, tags, filter metadata, existing SVG links, analytics tables, and URL-backed UI mode. - Namespace Layer: Logic is encapsulated in
App,Data,UI,Actions,Utils, andTemplatesobjects. - Event Layer: Input handling is split between document-level delegation, targeted control listeners, popover menus, and keyboard navigation handlers.
- Data flow: Static JSON →
Promise.allfetch/init →App.statemaps/analytics →Data.process()→UI.render()/UI.loadMore()→Templates/ DOM.
- Filter JSON files are keyed by filter ID.
- Entries may be simple component-name strings or objects with metadata such as
idandexisting_drawable. unlabeledis computed at runtime from requests that have no other tags.
firstAppearanceandlastRequestedare Unix timestamps (seconds since epoch).installsis a string with commas and a plus sign (e.g., "100,000,000+").
{
"count": 1,
"lastUpdate": "2026-01-01",
"apps": [
{
"drawable": "drawable_in_snake_case",
"requestCount": 100,
"firstAppearance": 1767196800.0,
"lastRequested": 1767196800.0,
"label": "App Title",
"componentName": "com.example/com.example.MainActivity",
"installs": "100,000,000+"
},
{
"drawable": "subway_city",
"requestCount": 50,
"firstAppearance": 1767196800.0,
"lastRequested": 1767196800.0,
"label": "App Title",
"componentName": "com.example.foo/com.example.foo.MainActivity",
"installs": "500,000+"
}
]
}<resources>
<item component="ComponentInfo{COMPONENT_NAME}" drawable="ICON_NAME" name="APP_NAME" />
<!-- Example -->
<item component="ComponentInfo{app.lawnchair.lawnicons/app.lawnchair.lawnicons.MainActivity}" drawable="lawnicons" name="Lawnicons" />
</resources>Allowed:
- Small, single-purpose utility libraries (e.g.,
fflatefor compression). - CDNs for external libraries.
Forbidden:
- UI Frameworks (React/Vue/Angular).
- CSS-in-JS libraries.
- Node.js build-time requirements for the dashboard itself (must run via static server).
Functional:
- URL parameters must be the source of truth for
q,view,sort,regex,filters, andgeoon page load. - Selection state (
App.state.selected) must persist through search, filter, sort, and view updates. - Regex mode and geo-batch mode must remain mutually exclusive in UI state and control visibility.
- Exported filenames in ZIP must match the generated
drawablevalues written toappfilter.xml,icontool_commands.txt, and PR/link metadata. - Drawable collision handling must stay deterministic: the same label/package pair reuses one drawable, while conflicting names for different apps receive stable suffixes.
Architectural:
- String HTML fragments should reside in the
Templatesobject; direct DOM creation is acceptable for small controls, observers, and focus/tooltip plumbing. - DOM manipulation must be batch-processed using
DocumentFragment. - The
App.state.idMapmust maintain O(1) lookup for all loaded apps. App.state.appTags,filterMetadata, and analytics maps are the canonical lookup sources after initialization.
Performance:
- Max startup time: < 500ms to first meaningful paint.
- Critical hot paths:
Data.process(filtering loop) andUI.loadMore(rendering batch).
- Identify: Check whether the change is Build-time (Python/data generation) or Runtime (JS/UI/export) work.
- State: Update
App.stateand related lookup initialization if the runtime data shape changes. - JSDoc: Update
@typedef/@typeannotations inscript.jswhen adding state, filter metadata, or dataset fields. - URL Sync: If behavior is deep-linkable, update both
Data.loadUrlState()andData.syncUrlState(). - Processing Path: If search, tags, sort, or geo batching change, update
Utils.parseSearchQuery(),Data.process(), and any dependent UI controls together. - Exports: If bundle contents or drawable naming change, keep
appfilter.xml,filter_config.json,icontool_commands.txt, and PR output synchronized. - Accessibility: Ensure new interactive elements have
tabindex,role, and keyboard listeners or menu navigation support.
Formatting:
- CSS follows Material Design 3 naming conventions for tokens (Color, Shape, Elevation).
- Typography uses
remunits for accessibility.
Naming Conventions:
- Namespaces: PascalCase (e.g.,
App.UI). - Functions: camelCase (e.g.,
createListRow). - Templates: camelCase (e.g.,
Templates.gridCard). - CSS Classes: kebab-case (e.g.,
list-row,status-wip).
Error Handling:
- Use
Toast.show(message, "error")for user-facing issues. - Keep external asset failures non-fatal by routing image fallbacks through
Utils.handleImageError()or equivalent guarded behavior.
- Type checks: Managed via
// @ts-checkand JSDoc in VS Code. - Manual Validation:
- Verify shift-click range selection and keyboard selection in both list and grid views.
- Verify URL restore for
q,view,sort,regex,filters, andgeo. - Verify invalid regex input fails safely without crashing the app.
- Verify geo-batch configuration toggles correctly and remains mutually exclusive with regex mode.
- Verify ZIP contents for
appfilter.xml,filter_config.json,icontool_commands.txt, andpr_description.mdin bothnewandlinkflows. - Verify horizontal scroll and popover usability on mobile (< 640px).
- requests.json: The primary database.
- Filter JSONs: Provide labels/descriptions and per-app tag membership, optionally with
existing_drawablemetadata. - Analytics JSONs:
sets_stats.json,creation_odds.json,domain_stats.json, andactivity_stats.jsonenrich ranking and dashboard cards. - URL: Query parameters (
q,view,sort,regex,filters,geo) act as environment state.
- Simplicity: Prefer readable vanilla JS over clever abstractions.
- Performance: Prioritize smooth 60fps scrolling over complex animations.
- Determinism: The same URL must always yield the same UI state.
- Developer Experience: Use JSDoc to make the code accessible to junior contributors.
- DOM Collisions: Wiping
innerHTMLis acceptable at the start of a full render, but incremental list growth must still happen throughloadMore()appending to avoid jank. - Event Delegation: Use
e.target.closest()carefully across SVG/icon children and popover menu items; click handling is no longer limited to the root container. - Mode Coupling: Regex mode and geo-batch mode share the same search affordance and can drift out of sync if visibility and state toggles are not updated together.
- Filter Data Shape: Filter files may contain strings or objects; code that assumes one shape will silently drop metadata like
existing_drawable. - Regex: Invalid regex input in search must be caught via
try/catchto prevent app crash.
Allowed:
- UI polish and M3 alignment.
- New local-only Python scripts for data cleaning.
- New filter definitions in
CONFIG.data.filters.
Requires explicit approval:
- Adding new heavy JS dependencies.
- Re-introducing nested data structures in
requests.json. - Modifying the core
Data.process()loop or bulk export file contract.