--- name: web-components description: Architecture and coding rules for single-page applications using web components, BCE layering, lit-html templating, Redux Toolkit state management, and client-side routing. Web standards and web platform first, minimal external dependencies. Use when creating, scaffolding, generating, writing, or reviewing web component applications, custom elements, state management, client-side routing, or frontend BCE architecture. Not for server-side rendering or framework-heavy applications. --- Build or maintain a web component application using $ARGUMENTS. Apply all rules below strictly. ## Guiding Principles - web standards and web platform first - minimal external dependencies — every dependency must justify its existence - no frameworks — use the platform: custom elements, ES modules, import maps - no build step required for development — bundling is only for packaging third-party dependencies - progressive enhancement over JavaScript-first design ## Reference Implementation This skill is based on the [bce.design](https://github.com/AdamBien/bce.design) quickstarter. ## Allowed Dependencies Only these three runtime dependencies are permitted. Do not add others without explicit approval: 1. **lit-html** — templating and efficient DOM rendering 2. **Redux Toolkit** — predictable state management with unidirectional data flow 3. **Vaadin Router** — client-side routing via History API Development tooling: Vite (dev server), Rollup (dependency bundling), Playwright (E2E tests). ## BCE Architecture (Boundary Control Entity) Structure code using the Boundary Control Entity pattern. Organize by feature module, not by technical layer: ``` app/src/ ├── BElement.js # base class for all custom elements ├── store.js # Redux store configuration ├── app.js # entry point, routing, persistence ├── app.config.js # application metadata (name, version) ├── index.html # single HTML entry point ├── style.css # application styles ├── libs/ # bundled third-party ESM modules ├── [feature-name]/ # one folder per feature module │ ├── boundary/ # UI web components │ ├── control/ # actions and dispatchers │ └── entity/ # reducers and state shape └── localstorage/ └── control/ └── StorageControl.js # persistence layer ``` ### Boundary (UI Components) - all components extend `BElement` — never extend `HTMLElement` directly - components are the only layer that imports `html` from lit-html - components dispatch actions through control functions — never dispatch directly to the store - override `view()` to return a lit-html template — this is the only required method - override `extractState(reduxState)` to select the relevant state slice - access extracted state via `this.state` inside `view()` - register every component with `customElements.define('prefix-name', ClassName)` - use `b-` prefix for component tag names - composite components import and compose child components ```javascript import BElement from "../../BElement.js"; import { html } from "lit-html"; import { deleteItem } from "../control/CRUDControl.js"; class ItemList extends BElement { extractState({ items: { list } }) { return list; } view() { return html`
    ${this.state.map(item => html`
  1. ${item.label}
  2. `)}
`; } } customElements.define('b-item-list', ItemList); ``` ### Control (Actions & Dispatchers) - define Redux actions with `createAction` from Redux Toolkit - export both the action creator and a dispatcher function for each action - dispatcher functions encapsulate `store.dispatch()` — boundary components call dispatchers, never dispatch directly - use `Date.now()` for generating entity IDs on the client ```javascript import { createAction } from "@reduxjs/toolkit"; import store from "../../store.js"; export const itemUpdatedAction = createAction("itemUpdatedAction"); export const itemUpdated = (name, value) => { store.dispatch(itemUpdatedAction({ name, value })); } export const newItemAction = createAction("newItemAction"); export const newItem = _ => { store.dispatch(newItemAction(Date.now())); } export const deleteItemAction = createAction("deleteItemAction"); export const deleteItem = (id) => { store.dispatch(deleteItemAction(id)); } ``` ### Entity (Reducers & State) - use `createReducer` from Redux Toolkit with builder callback - define `initialState` as a plain object with sensible defaults - use a temporal cache object pattern for form input before committing to a list - keep state shape flat — avoid deep nesting ```javascript import { createReducer } from "@reduxjs/toolkit"; import { itemUpdatedAction, deleteItemAction, newItemAction } from "../control/CRUDControl.js"; const initialState = { list: [], item: {} } export const items = createReducer(initialState, (builder) => { builder.addCase(itemUpdatedAction, (state, { payload: { name, value } }) => { state.item[name] = value; }).addCase(newItemAction, (state, { payload }) => { state.item["id"] = payload; state.list = state.list.concat(state.item); }).addCase(deleteItemAction, (state, { payload }) => { state.list = state.list.filter(item => item.id !== payload); }); }); ``` ## BElement Base Class The base class provides automatic Redux integration for all components: - `connectedCallback()` — subscribes to Redux store, triggers initial render - `disconnectedCallback()` — unsubscribes to prevent memory leaks - `triggerViewUpdate()` — extracts state, generates template, renders via lit-html - `view()` — abstract, must be overridden to return a lit-html template - `extractState(reduxState)` — override to select a state slice (default: entire state) - `getRenderTarget()` — override to change render target (default: `this`) Do not modify the BElement base class unless adding cross-cutting concerns that affect all components. ## State Management ### Store Configuration ```javascript import { configureStore } from "@reduxjs/toolkit"; import { load } from "./localstorage/control/StorageControl.js"; import { items } from "./items/entity/ItemsReducer.js"; const reducer = { items } const preloadedState = load(); const config = preloadedState ? { reducer, preloadedState } : { reducer }; const store = configureStore(config); export default store; ``` ### localStorage Persistence - persist entire Redux state to localStorage on every store update via `store.subscribe()` - use `JSON.stringify` / `JSON.parse` for serialization - derive storage key from `appName` in `app.config.js` - wrap save/load in try-catch — never let persistence errors crash the application - preload persisted state on store initialization ### Data Flow ``` User Event → Boundary Component → Control (dispatcher) → Redux Action → Entity (reducer) → Store → BElement.triggerViewUpdate() → extractState() → view() → lit-html render() → DOM ``` ## Routing - use Vaadin Router with an HTML element as outlet - define routes mapping paths to custom element tag names - import all routed components before initializing the router - use standard `` for navigation — no programmatic routing unless necessary ```javascript import { Router } from "@vaadin/router"; const outlet = document.querySelector('.view'); const router = new Router(outlet, {}); router.setRoutes([ { path: '/', component: 'b-item-list' }, { path: '/add', component: 'b-items' } ]); ``` ## HTML Structure - single `index.html` entry point with semantic HTML (`
`, `
`, `