Widget development in Announcable. Use when creating, modifying, or debugging the embeddable widget (Lit, TypeScript, Vite, UMD).
All commands run from widget/:
cd widget
Run after every change. Do NOT trust your own assessment — verify through observable behavior.
npm run lint # Must pass
npm run build # Must succeed (UMD bundle)
widget/
├── src/
│ ├── main.ts # Entry point, registers <announcable-widget> custom element
│ ├── app.ts # Main widget LitElement component
│ ├── index.css # Base widget styles
│ ├── components/
│ │ ├── ui/ # Generic UI components
│ │ ├── widget/ # Widget-specific view components
│ │ └── icons/ # SVG icon components
│ ├── controllers/ # Lit reactive controllers
│ │ ├── anchors.ts # Anchor link behavior
│ │ └── widget-toggle.ts # Widget open/close toggle
│ ├── tasks/ # Async data fetching (Lit Task pattern)
│ │ ├── release-notes.ts # Fetch release notes from API
│ │ ├── release-note-likes.ts
│ │ ├── release-note-metrics.ts
│ │ ├── release-note-status.ts
│ │ └── widget-config.ts # Fetch widget configuration
│ └── lib/ # Utilities and shared code
│ ├── base-component.ts # Base class for all components
│ ├── clientId.ts # Anonymous client identification
│ ├── config.ts # Backend URL configuration
│ ├── contexts.ts # Lit context definitions
│ ├── storage.ts # Local storage helpers
│ ├── types.ts # Shared TypeScript types
│ └── utils.ts # General utilities
├── vite.config.ts # Builds as UMD library
└── package.json
The widget is a self-contained Lit web component application:
main.ts): Registers <announcable-widget> custom elementapp.ts): Root LitElement that orchestrates data fetching and renderingTask pattern for async data fetching from the backend APIBuilt as UMD bundle (widget.js) with CSS injected by JS (no separate CSS file). Embedded on customer websites via:
<script src="https://announcable.com/widget?id=ORG_ID"></script>
The widget fetches data from the backend API at /api/:
| Endpoint | Task | Purpose |
|---|---|---|
GET /api/release-notes/{orgId} | release-notes.ts | Fetch published release notes |
GET /api/release-notes/{orgId}/status | release-note-status.ts | Check for new notes |
POST /api/release-notes/{orgId}/metrics | release-note-metrics.ts | Track views |
GET /api/release-notes/{orgId}/{id}/like | release-note-likes.ts | Get like state |
POST /api/release-notes/{orgId}/{id}/like | release-note-likes.ts | Toggle like |
GET /api/widget-config/{orgId} | widget-config.ts | Get widget appearance config |
All components extend BaseComponent from lib/base-component.ts which provides shared functionality.
Widget-wide state is shared via Lit contexts defined in lib/contexts.ts. The root app.ts provides context values consumed by child components.
Data fetching uses Lit's Task class for declarative async operations:
// tasks/release-notes.ts
export const releaseNotesTask = new Task(host, {
task: async ([orgId]) => {
const response = await fetch(`${config.backendUrl}/api/release-notes/${orgId}`);
return response.json();
},
args: () => [host.orgId],
});
All widget CSS is namespaced to .announcable-widget to avoid conflicts when embedded on customer websites. Use Shadow DOM encapsulation where possible.
# Terminal 1: Start backend dev services
cd backend && make dev-services
# Terminal 2: Start backend
cd backend && make dev-air
# Terminal 3: Start widget dev server
cd widget && npm run dev
npm run dev # Dev server with hot-reload
npm run build # Production UMD bundle
npm run build:test # Dev/test build
npm run lint # Lint check
components/ui/ or components/widget/)BaseComponent or LitElement@customElement, @property, @state)import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('my-component')
export class MyComponent extends LitElement {
@property() label = '';
static styles = css`
:host { display: block; }
`;
render() {
return html`<div>${this.label}</div>`;
}
}
src/tasks/Task patternnpm run lint passesnpm run build succeedsany types introduced| Don't | Why | Instead |
|---|---|---|
| Use global CSS | Leaks into host page | Use Shadow DOM or .announcable-widget namespace |
| Fetch directly in render | Re-fetches on every render | Use Lit Task pattern |
Use any type | Hides errors | Use proper TypeScript types from lib/types.ts |
| Hardcode backend URL | Breaks in different environments | Use lib/config.ts |
| Modify DOM outside Shadow DOM | Unpredictable on host pages | Keep all DOM in component templates |
| Batch changes | Hard to identify breakage | One change → validate |