Jump to content

CentralNotice/BannerEditorDeveloperGuide

From Meta, a Wikimedia project coordination wiki

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]
[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
  1. lastModified
  2. 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:

  1. Box (close icon container)
  2. Actual close icon
Close button editable styles:
[edit]
  • Box styles:
  1. Position
  2. Background color
  3. Border
  4. Border radius
  5. Padding
  6. Margin
  7. Opacity
  8. Box shadow
  • Icon styles:
  1. Size
  2. Color
  3. Stroke width

Adding new templates

[edit]

The current template system uses factory functions with default values for:

  1. Easy customization of templates - Spread and override pattern
  2. DRY principle - Reusable defaults across templates
  3. Ensures all properties exist - Critical for reactivity
  4. 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:

  1. Visit: https://gitlab.wikimedia.org/toolforge-repos/centralnotice-banner-editor
  2. Click the Fork button at the top right
  3. Under "Project URL", select your username
  4. 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
[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.