CentralNotice/BannerEditorDeveloperGuide
Overview
[edit]The CentralNotice Banner Editor is a web application for creating and editing banners using a visual editor instead of writing banner code manually. It provides:
- WYSIWYG editor - Real-time visual editing with live preview
- Responsive design - Create banners with mobile, tablet, and desktop layouts
- Template system - Pre-built templates for quick banner creation
- Code generation - Exports designed banner code for CentralNotice
- Local persistence - Banners saved to browser localStorage
Tech stack
[edit]- Vue 3.5 (Composition API)
- TypeScript - Full type safety
- Pinia - State management
- Vite - Build tooling
- Codex - Wikimedia Design System
- Less - For styling
Project structure
[edit]src/ ├── assets/ # Static assets (images, fonts) ├── components/ # Vue components ├── composables/ # Vue composables (shared logic) ├── constants/ # Application constants ├── codeGenerator/ # Banner code generation logic ├── router/ # Vue Router configuration ├── store/ # Pinia stores ├── styles/ # Global styles ├── templates/ # Banner templates ├── types/ # TypeScript type definitions ├── utils/ # Utility functions ├── App.vue # Root component └── main.ts # Application entry point
Architecture
[edit]Data flow
[edit]┌─────────────┐
│ User │
└──────┬──────┘
│
▼
┌─────────────────────┐
│ Vue Components │ ← UI layer
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ Pinia Stores │ ← State management
│ - bannersStore │
│ - templatesStore │
│ - favoritesStore │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ localStorage │ ← Persistence
└─────────────────────┘
State management
[edit]Banner lifecycle
[edit]1. User clicks "Create Banner" or selects template
↓
2. New banner created in bannersStore
banner = { id, name: "Untitled", template: undefined, ... }
↓
3. Template selected in templatesStore
template cloned and assigned to banner
↓
4. User edits template via editor UI
Changes saved to selectedTemplate (reactive)
↓
5. Changes auto-saved to localStorage
Watcher in bannersStore triggers on changes to banners (including template modifications)
↓
6. User navigates away
Banner persists in localStorage
↓
7. User returns later
Banner loaded from localStorage by ID
Core concepts
[edit]1. Banners
[edit]User-created banners that contain:
- Unique ID
- Name
- Template instance (styles + properties)
- Thumbnail for banner preview in landing page
- Metadata
- lastModified
- isFavorite
type Banner = {
id: string;
name: string;
template?: BannerTemplate;
thumbnail?: BannerThumbnail;
lastModified: number;
isFavorite: boolean;
}
2. Templates
[edit]Predefined banner designs with:
- Unique ID
- Name
- Thumbnail for template preview in template menu
- Editable responsive styles (mobile/tablet/desktop)
- Editable properties (text, images, links)
- Fixed CSS: uneditable styles that can be used for adding effects like hover effects, etc
type BannerTemplate = {
id: string;
name: string;
thumbnail: string;
styles: TemplateStyles;
properties: TemplateProperties;
fixedCss: string;
}
3. Viewports
[edit]Banner designs can be:
- Responsive: Different styles for mobile/tablet/desktop
- One-size: Single design for all screen sizes. Uses the desktop template style by default under the hood.
Template system
[edit]Allowed properties by element types (shared across viewports)
[edit]- Banner: Link, text direction
- Images: Source, alt text
- Texts: Text content, URL
Note: Properties contain content data, while styles contain visual presentation.
Allowed banner element quantity
[edit]- Banner itself: Just 1
- Image: 0 or more
- Text: 0 or more
- Close button: Just 1
Styling banner elements
[edit]Banner element styles are viewport specific. Each style is contained in a viewport object, which is used to apply respective styles to the banner at certain viewports.
For a complete list of all available style properties, see the type definitions in src/types/templates.ts.
type TemplateStyles = {
mobile: TemplateViewportStyles;
tablet: TemplateViewportStyles;
desktop: TemplateViewportStyles;
}
Important
[edit]The close button consists of two styleable components:
- Box (close icon container)
- Actual close icon
Close button editable styles:
[edit]- Box styles:
- Position
- Background color
- Border
- Border radius
- Padding
- Margin
- Opacity
- Box shadow
- Icon styles:
- Size
- Color
- Stroke width
Adding new templates
[edit]The current template system uses factory functions with default values for:
- Easy customization of templates - Spread and override pattern
- DRY principle - Reusable defaults across templates
- Ensures all properties exist - Critical for reactivity
- Maintainability - Change defaults in one place
High-level factory
[edit]createTemplateStarter(): BannerTemplate
Generates a complete base template with all properties initialized. Use as starting point for templates with spread operator.
All factory functions located in src/templates/templateStarter.ts
Step-by-step guide
[edit]1. Create template file
[edit]Create src/templates/template_name.ts
2. Import factory functions
[edit]Import desired factory functions for template defaults.
Example
[edit]import {
createTemplateStarter,
createBannerStyles,
createBannerImageStyles,
createBannerTextStyles,
createBannerCloseButtonStyles,
createBannerProperties,
createBannerImageProperties,
createBannerTextProperties,
createSizeDefaults
} from './templateStarter';
3. Ensure type safety and unique ids by importing appropriate types and createId function
[edit]Example
[edit]import { createId } from '@/utils/utils';
import type { BannerTemplate } from '@/types/templates';
4. Import template thumbnail
[edit]Place thumbnail image in src/assets/templates or use a base64 data string.
import templateNameThumbnail from '@/assets/templates/template_name.svg';
5. Define template structure
[edit]See src/types/templates.ts for the type definition of all possible values to know what to pass.
const templateName: BannerTemplate = {
// Inherit defaults structure
...createTemplateStarter(),
// Override metadata
id: createId(),
name: 'Template Name',
thumbnail: templateNameThumbnail,
// Define responsive styles by overriding desired defaults styles
styles: {
mobile: {
banner: {
...createBannerStyles(createId()),
// Override defaults
height: { value: 120, unit: 'px' },
},
images: [
{
...createBannerImageStyles(createId()),
position: {
mode: 'alignment',
alignment: {
horizontal: 'center',
vertical: 'middle'
}
}
}
],
texts: [
{
...createBannerTextStyles(createId()),
fontSize: { value: 18, unit: 'px' },
}
],
closeButton: createBannerCloseButtonStyles(createId())
},
tablet: { /* Define styles for tablet viewport */ },
desktop: { /* Define styles for desktop viewport */ }
},
// Define content properties (shared across viewports)
// Can also start with defaults and override desired properties
properties: {
banner: {
...createBannerProperties(createId()),
link: '//donate.wikimedia.org',
textDirection: 'ltr'
},
images: [
{
...createBannerImageProperties(createId()),
type: 'url',
source: '//upload.wikimedia.org/wikipedia/commons/thumb/8/81/Wikimedia-logo.svg/50px-Wikimedia-logo.svg.png',
accessibleText: 'Wikimedia Foundation logo'
}
],
texts: [
{
...createBannerTextProperties(createId()),
text: 'Support Wikipedia. Donate today.'
}
]
},
// Optional: Custom fixed CSS
fixedCss: `
.cnotice-full-banner-click:hover .cnotice-text-1 {
text-decoration: underline;
}
`
};
6. Export template
[edit]export default templateName;
7. Register template
[edit]Update src/templates/templates.ts:
import templateV2 from './template_v2';
import templateName from './template_name'; // Import new template
const templates = [
templateV2,
templateName // Add to registry
];
export default templates;
Template best practices
[edit]Code patterns
[edit]Good: Use spread operator for factory defaults
[edit]{
...createBannerStyles(createId()),
backgroundColor: '#custom'
}
Bad: Manual object creation (missing properties)
[edit]{
id: createId(),
backgroundColor: '#custom'
// Missing: height, padding, margin, etc.
}
Good: Generate unique IDs for each element
[edit]images: [
createBannerImageStyles(createId()), // Unique ID
createBannerImageStyles(createId()) // Unique ID
]
Bad: Reusing IDs
[edit]const imgId = createId();
images: [
createBannerImageStyles(imgId), // Same ID
createBannerImageStyles(imgId) // Causes issues like wrong element selection in the editor
]
Multiple elements
[edit]Important: Ensure arrays match across viewports and properties:
// All 3 viewports should have matching array lengths
styles: {
mobile: { images: [ /* 2 items */ ] },
tablet: { images: [ /* 2 items */ ] },
desktop: { images: [ /* 2 items */ ] }
}
Type annotations
[edit]Use TypeScript type annotations to ensure valid values are assigned to properties and catch errors at compile time.
Good: Annotate with appropriate types to catch errors early
[edit]import type { BannerTemplate, BannerTextStyles, Dimension } from '@/types/templates';
// Explicitly type the template
const templateName: BannerTemplate = {
...createTemplateStarter(),
name: 'Template Name',
styles: {
mobile: {
banner: {
...createBannerStyles(createId()),
// TypeScript ensures correct Dimension structure
height: { value: 120, unit: 'px' },
},
texts: [
{
...createBannerTextStyles(createId()),
// TypeScript validates this is a valid FontWeight ('100'-'900')
fontWeight: '700',
// Correct Dimension object with value and unit
fontSize: { value: 18, unit: 'px' }
}
]
}
}
};
Bad: Missing type annotations allow incorrect data structures
[edit]// No type annotation - TypeScript can't catch errors
const templateName = {
...createTemplateStarter(),
name: 'Template Name',
styles: {
mobile: {
banner: {
...createBannerStyles(createId()),
// Wrong: Number instead of Dimension object
// TypeScript won't catch this without type annotation
height: 120, // Should be { value: 120, unit: 'px' }
},
texts: [
{
...createBannerTextStyles(createId()),
// Wrong: Invalid font weight value
fontWeight: 'super-bold', // Should be '100'-'900'
// Wrong: Number instead of Dimension
fontSize: 18 // Should be { value: 18, unit: 'px' }
}
]
}
}
};
Common mistakes and corrections:
[edit]Bad: Number instead of Dimension
[edit]height: 100
Good: Use dimension object to support multiple units
[edit]
height: { value: 100, unit: 'px' }
Bad: Invalid fontWeight string
[edit]fontWeight: 'extra-bold'
Good: Valid FontWeight value ('100'-'900')
[edit]
fontWeight: '700'
Bad: Invalid alignment values
[edit]alignment: { horizontal: 'middle', vertical: 'left' }
Good: Valid alignment values
[edit]alignment: { horizontal: 'center', vertical: 'top' }
Generated code output
[edit]Templates are converted to raw code when the button that triggers code generation is clicked in the editor. The output includes:
- HTML: Semantic markup with accessibility attributes
- CSS: Scoped styles with media queries for responsive layouts
- Minimal JavaScript: Such as inline event handlers
- Selector scoping: The CSS selectors are preceded with the banner ID to prevent banner styles from affecting other content on the page they are rendered
Local setup
[edit]Prerequisites
[edit]- Node.js: >=18 <23
- npm: Any version compatible with your Node.js version (recommended: latest stable for your Node version)
Using a version outside this range may result in errors such as:
npm WARN EBADENGINE Unsupported engine {
package: 'eslint-config-wikimedia@0.30.0',
required: { node: '>=18 <23' },
current: { node: 'v23.0.0', npm: '10.9.0' }
}
You can use nvm (Node Version Manager) to manage and switch Node versions easily.
Installation
[edit]Option 1: For those with no developer access
[edit]Fork the repository
[edit]If you don't have developer access to the main project repository, start by forking it to your own GitLab account:
- Visit: https://gitlab.wikimedia.org/toolforge-repos/centralnotice-banner-editor
- Click the Fork button at the top right
- Under "Project URL", select your username
- Click Fork project
Then clone your fork
[edit]Replace <your-username> with your GitLab username:
git clone git@gitlab.wikimedia.org:<your-username>/centralnotice-banner-editor.git
Option 2: For those with access
[edit]Clone the repository directly
[edit]git clone git@gitlab.wikimedia.org:toolforge-repos/centralnotice-banner-editor.git
Navigate to project directory after cloning
[edit]cd centralnotice-banner-editor
Install dependencies
[edit]npm install
Usage
[edit]Start a local development server
[edit]npm run dev
Build a production-ready version of the tool
[edit]npm run build
Testing and Linting
[edit]Please ensure the following checks pass before submitting a change:
Type checking
[edit]npm run type-check
Lint JavaScript, TypeScript, and Vue files
[edit]npm run lint
Lint CSS and LESS files
[edit]npm run lint:css
Run all linting scripts
[edit]npm run lint:all
Submitting changes
[edit]Refer to the Wikimedia GitLab workflow for submitting changes.