@mieweb/news-widget
v1.0.0-alpha3
Published
Embeddable Instagram-style news feed widget with video playback, swipe gestures, and real-time comments
Downloads
64
Readme
News Widget - Instagram-Style Video Feed
An embeddable, responsive news feed component with Instagram-style video playback, swipe gestures, and real-time comments. Built with React 19, TypeScript, and Vite.
✨ Features
- 📱 Instagram-style UI - Vertical scrolling feed with full-screen video viewer
- 🎥 Multi-format media - Supports YouTube videos, MP4 files, and images
- 👆 Touch-optimized - Swipe gestures for navigation (mobile-first)
- 🎬 Auto-play - Videos play automatically when visible, pause when scrolled away
- 💬 Real-time comments - Fetch and post comments via Discourse integration
- 🎨 Themeable - Respects parent page color schemes via CSS custom properties
- ♿ Accessibility-first - Full ARIA support, keyboard navigation, screen reader tested
- 🌐 RSS-powered - Parses standard RSS feeds with media enclosures
🚀 Quick Start
Development
cd news-widget
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # Build for production
npm run preview # Preview production buildTesting
npm test # Run Playwright E2E tests
npm run test:ui # Open Playwright UI
npm run test:headed # Run tests in headed mode📦 Embedding the Widget
Option 1: NPM Package (Recommended for React Apps)
Install the widget as an NPM dependency:
npm install @mieweb/news-widget
# or
yarn add @mieweb/news-widget
# or
pnpm add @mieweb/news-widgetUsing as a React Component
import { NewsWidget } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
function App() {
return (
<div className="my-app">
<h1>Latest News</h1>
{/* Show landing page with all feeds */}
<NewsWidget />
{/* Or render a specific feed directly */}
<NewsWidget feedId="features" />
</div>
);
}Using with Vanilla JavaScript
import { renderNewsWidget } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
// Show landing page with all feeds
renderNewsWidget(document.getElementById('news-feed'));
// Or render a specific registered feed directly (no landing page)
renderNewsWidget(document.getElementById('news-feed'), { feedId: 'features' });Advanced Usage: Custom Hooks & Components
import { useFeed, Feed, FeedCard } from '@mieweb/news-widget';
import '@mieweb/news-widget/style.css';
import type { Post } from '@mieweb/news-widget';
function CustomFeed() {
const { posts, loading, error } = useFeed('https://example.com/feed.rss');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts.map((post: Post) => (
<FeedCard key={post.id} post={post} />
))}
</div>
);
}Option 2: Build from Source
Build the widget yourself for full control:
npm run buildThis creates optimized files in the dist/ folder:
dist/index.html- Main HTML entry pointdist/assets/*.js- JavaScript bundlesdist/assets/*.css- Stylesheets
Option 3: Basic HTML Embedding (No Build Tools)
Copy the dist/ folder to your web server and embed with an iframe:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<style>
/* Make iframe responsive and full-height */
.news-widget-container {
width: 100%;
height: 600px; /* Or use 100vh for full viewport */
border: none;
overflow: hidden;
}
</style>
</head>
<body>
<h1>Latest News</h1>
<!-- Embed the news widget -->
<iframe
src="/dist/index.html"
class="news-widget-container"
title="News Feed Widget"
sandbox="allow-scripts allow-same-origin allow-popups"
></iframe>
</body>
</html>Embedding a Specific Feed via iframe
To show a specific feed without the landing page, use the IIFE build with feedId:
<!-- widget.html -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="news-widget.iife.js"></script>
<script>
var params = new URLSearchParams(window.location.search);
var feedId = params.get('feedId');
NewsWidget.renderNewsWidget(
document.getElementById('root'),
feedId ? { feedId: feedId } : {}
);
</script>
</body>
</html>Then embed with a query parameter to select the feed:
<!-- Enterprise Health feeds (pre-registered) -->
<iframe src="widget.html?feedId=features" title="Features Feed"></iframe>
<iframe src="widget.html?feedId=testing" title="Testing Feed"></iframe>
<iframe src="widget.html?feedId=public" title="Public Feed"></iframe>Embedding a Custom Feed via iframe
Use registerFeed() to add a custom feed at runtime before rendering:
<!-- custom-widget.html -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="news-widget.iife.js"></script>
<script>
var params = new URLSearchParams(window.location.search);
var feedUrl = params.get('feed');
var feedName = params.get('name') || 'News';
if (feedUrl) {
NewsWidget.registerFeed({
id: 'custom',
name: feedName,
url: feedUrl,
description: '',
emoji: '📰',
capabilities: { supportsLikes: true, supportsComments: true }
});
}
NewsWidget.renderNewsWidget(
document.getElementById('root'),
feedUrl ? { feedId: 'custom' } : {}
);
</script>
</body>
</html><iframe src="custom-widget.html?feed=https://example.com/feed.rss&name=My+Feed"></iframe>Direct Integration (No iframe)
For tighter integration, include the built assets directly:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<!-- Include widget styles -->
<link rel="stylesheet" href="/dist/assets/index-[hash].css">
</head>
<body>
<!-- Widget mounts here -->
<div id="root"></div>
<!-- Include widget JavaScript -->
<script type="module" src="/dist/assets/index-[hash].js"></script>
</body>
</html>Note: Replace
[hash]with the actual hash from your build output.
Option 4: CDN (Coming Soon)
Use a CDN for quick prototyping without installation:
<link rel="stylesheet" href="https://unpkg.com/@mieweb/news-widget/style.css">
<script type="module">
import { renderNewsWidget } from 'https://unpkg.com/@mieweb/news-widget';
renderNewsWidget(document.getElementById('news-feed'));
</script>🎨 Customizing Colors & Styles
The widget uses the @mieweb/ui design system with Tailwind CSS 4 and CSS custom properties for theming. The default brand is BlueHive.
Theme Architecture
Theming is handled via @mieweb/ui brand CSS files imported in src/index.css:
@import '@mieweb/ui/brands/bluehive.css' layer(theme);
@import 'tailwindcss';All component colors use var(--mieweb-*) CSS custom properties, which are mapped from the brand's Tailwind color tokens in the @theme block.
Override Default Colors
Override the CSS custom properties on your parent page:
<style>
:root {
/* Primary brand colors */
--mieweb-primary-500: #0066cc;
--mieweb-primary-600: #0055aa;
/* Semantic tokens */
--mieweb-background: #ffffff;
--mieweb-foreground: #333333;
--mieweb-card: #f9f9f9;
--mieweb-border: #e0e0e0;
--mieweb-muted-foreground: #666666;
}
</style>Dark Mode Support
Dark mode is activated via data-theme="dark" attribute on a parent element:
<div data-theme="dark">
<!-- Widget renders in dark mode -->
</div>Or override CSS variables for dark mode:
<style>
@media (prefers-color-scheme: dark) {
:root {
--mieweb-background: #0a0a0a;
--mieweb-foreground: #fafafa;
--mieweb-card: #1a1a1a;
--mieweb-border: #2a2a2a;
--mieweb-muted-foreground: #a0a0a0;
}
}
</style>Available CSS Custom Properties
| Property | Default (Light) | Purpose |
|----------|-----------------|---------|
| --mieweb-background | #fafafa | Main background color |
| --mieweb-foreground | #0a0a0a | Primary text color |
| --mieweb-card | #ffffff | Card/container background |
| --mieweb-border | #e5e7eb | Border and divider color |
| --mieweb-muted-foreground | #737373 | Secondary/muted text |
| --mieweb-primary-500 | #3b82f6 | Primary accent (links, buttons) |
| --mieweb-destructive-500 | #ef4444 | Error/destructive actions |
| --mieweb-success-500 | #22c55e | Success indicators |
| --mieweb-ring | #3b82f6 | Focus ring color |
See src/index.css for the full list of color scale variables (--mieweb-primary-50 through --mieweb-primary-950, etc.).
Example: Brand Integration
Match your brand colors:
<style>
:root {
--mieweb-background: var(--your-site-bg, #f5f5f5);
--mieweb-primary-500: var(--your-brand-primary, #ff6b35);
--mieweb-foreground: var(--your-site-text, #2d3748);
}
</style>🔧 Configuration
RSS Feed Sources
Feeds can be configured statically in src/data/feedRegistry.ts, or registered at runtime.
Static Registration (build-time)
Add feeds to the FEED_SECTIONS array in feedRegistry.ts:
{
id: 'my-feed',
name: 'My News Feed',
description: 'Latest updates',
url: 'https://example.com/feed.rss',
emoji: '📰',
capabilities: { supportsLikes: true, supportsComments: true },
}Runtime Registration
Use registerFeed() to add or override feeds before rendering — useful for iframe embedding or dynamic configuration:
import { registerFeed, renderNewsWidget } from '@mieweb/news-widget';
registerFeed({
id: 'custom',
name: 'Custom Feed',
description: 'Dynamically registered',
url: 'https://example.com/feed.rss',
emoji: '📰',
capabilities: { supportsLikes: true, supportsComments: true },
});
renderNewsWidget(document.getElementById('root'), { feedId: 'custom' });Available Feed IDs
| Feed ID | Name | Source |
|---------|------|--------|
| features | Features | Enterprise Health product announcements |
| testing | Test | Enterprise Health testing discussions |
| public | Public | Enterprise Health public community |
| test-server | Test Server | Local development test server |
| sample | Sample Feed | Built-in demo content |
Proxy Configuration
For development, configure CORS proxies in vite.config.ts:
server: {
proxy: {
'/api/rss': {
target: 'https://your-discourse-instance.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/rss/, ''),
},
},
}📱 Mobile Optimization
The widget is mobile-first with touch gestures:
- Swipe up/down - Navigate between posts
- Tap video - Toggle play/pause
- Tap muted icon - Unmute audio
- Tap comment icon - Open comment panel
- Tap outside - Close comment panel
Responsive Breakpoints
/* Mobile: default styles */
/* Tablet: 768px+ */
/* Desktop: 1024px+ */♿ Accessibility
Built with WCAG 2.1 AA compliance:
- ✅ Full keyboard navigation (Tab, Enter, Escape)
- ✅ ARIA labels on all interactive elements
- ✅ Screen reader tested (VoiceOver, NVDA)
- ✅ Focus indicators on all controls
- ✅ Semantic HTML structure
- ✅ Color contrast meets AA standards
Keyboard Shortcuts
| Key | Action |
|-----|--------|
| Tab | Navigate between elements |
| Enter / Space | Activate buttons/links |
| Escape | Close comment panel or fullscreen viewer |
| Arrow Up/Down | Scroll feed (when focused) |
🧪 Testing
Playwright E2E tests verify:
- Video playback and autoplay
- Comment posting and syncing
- Like/unlike functionality
- Swipe gesture navigation
- Fullscreen viewer interactions
- Accessibility (ARIA roles, keyboard nav)
npm test # Run all tests
npm run test:ui # Interactive test UI
npm run test:headed # See tests in browser🏗️ Architecture
news-widget/
├── src/
│ ├── components/ # React components (using @mieweb/ui)
│ │ ├── Feed.tsx # Main feed container
│ │ ├── FeedCard.tsx # Individual post card (Card, CardHeader, CardActions)
│ │ ├── FullscreenViewer.tsx # Fullscreen video viewer (dialog)
│ │ ├── CommentsPanel.tsx # Comment sidebar (Input, Button)
│ │ ├── Avatar.tsx # User avatar (wraps @mieweb/ui Avatar)
│ │ ├── ClickTooltip.tsx # Tooltip trigger (wraps @mieweb/ui Tooltip)
│ │ └── LandingPage.tsx # Feed selection landing page (Card, Tooltip)
│ ├── hooks/ # Custom React hooks
│ │ ├── useFeed.ts # RSS feed fetching/parsing
│ │ ├── useComments.ts # Comment state management
│ │ ├── useVisibility.ts # IntersectionObserver for autoplay
│ │ ├── useRouter.ts # URL routing
│ │ └── useDiscourseAuth.ts # Authentication
│ ├── types/ # TypeScript interfaces
│ └── data/ # Feed configuration
└── test-server/ # Development test server🔒 Security Notes
When embedding via iframe, use appropriate sandbox attributes:
<iframe
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
src="/dist/index.html">
</iframe>allow-scripts- Required for JavaScript executionallow-same-origin- Required for API calls to parent domainallow-popups- For external linksallow-forms- For comment submission
Content Security Policy (CSP)
The IIFE build injects CSS at runtime by creating a <style> element via JavaScript. This requires the style-src 'unsafe-inline' directive (or a matching nonce/hash) in the page's Content Security Policy. If your site uses a strict CSP that disallows inline styles, the IIFE bundle's CSS will be blocked.
For environments with strict CSP, use the ES or UMD build with the separate dist/news-widget.css stylesheet instead.
📝 License
[Add your license here]
🤝 Contributing
See project copilot-instructions.md for code quality guidelines.
👨💻 Development
Tech Stack
- React 19 - Latest React with concurrent features
- TypeScript 5.9 - Type-safe development
- Vite 7 - Fast build tool and dev server
- @mieweb/ui - MIE design system components (Button, Card, Avatar, Tooltip, Input, Alert, etc.)
- Tailwind CSS 4 - Utility-first CSS framework (via
@tailwindcss/viteplugin) - lucide-react - SVG icon library (consistent with @mieweb/ui)
- react-player v3 - Multi-format video playback
- react-swipeable - Touch gesture handling
- Playwright - E2E testing
Project Commands
npm run dev # Start dev server (http://localhost:5173)
npm run build # Production build → dist/ (for standalone app)
npm run build:lib # Library build → dist/ (for NPM package)
npm run preview # Preview production build
npm run lint # Run ESLint
npm test # Run Playwright tests
npm run test:ui # Open Playwright UI for debuggingBuilding for NPM
To build the library version for NPM distribution:
npm run build:libThis generates:
dist/news-widget.js- ES moduledist/news-widget.umd.cjs- UMD module (browser globals)dist/news-widget.iife.js- Standalone IIFE bundle (all CSS inlined)dist/news-widget.css- Compiled styles (for ES/UMD consumers)dist/index.d.ts- TypeScript declarations
The IIFE build injects all CSS (Tailwind, component styles, @mieweb/ui) into the page at runtime via a <style> tag, so no separate stylesheet is needed — just a single <script> tag.
Publishing to NPM
Automated Publishing (Recommended)
Create a GitHub Release and the package is automatically published via GitHub Actions:
graph LR
updateVersion[Update Version] --> commitPush[Commit & Push]
commitPush --> createRelease[Create GitHub Release]
createRelease --> githubActions{GitHub Actions}
githubActions --> runTests[Run Tests]
githubActions --> runLinter[Run Linter]
githubActions --> buildLibrary[Build Library]
runTests --> checkPass{All Pass?}
runLinter --> checkPass
buildLibrary --> checkPass
checkPass -->|Yes| publishNPM[Publish to NPM]
checkPass -->|No| failed[❌ Failed]
publishNPM --> published[✅ Published]Steps:
- Update version in package.json (
npm version patch/minor/major) - Commit and push the version bump
- Create a GitHub Release with tag
vX.Y.Z - GitHub Actions workflow automatically runs and publishes to NPM
Manual Publishing
# Test the package locally first
npm run build:lib
npm pack
# Publish to NPM (requires auth)
npm login
npm publish --access publicResources:
- 🔧 EXAMPLES.md - Usage examples
Code Quality
This project follows strict quality guidelines:
- DRY principle - No code duplication
- KISS principle - Simplest solution that works
- Accessibility-first - ARIA labels, keyboard navigation
- Test-driven - E2E tests for all features
- Type-safe - Full TypeScript coverage
See .github/copilot-instructions.md for complete guidelines.
ESLint Configuration
The project uses flat config ESLint 9 with TypeScript support. To enable stricter type-aware rules:
// eslint.config.js
import tseslint from 'typescript-eslint';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
tseslint.configs.recommendedTypeChecked,
// or tseslint.configs.strictTypeChecked for stricter rules
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
])