From eb857ac9866bf289b7b4ff73d8aeb17314342093 Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Wed, 28 Jan 2026 15:58:58 +0000 Subject: [PATCH 1/2] Initial implementation of type generation --- package.json | 31 +++++- src/InteractiveMap/InteractiveMap.js | 113 ++++++++++++++++++++ src/config/defaults.js | 45 ++++++++ src/config/events.js | 55 +++++++++- src/index.js | 11 +- src/types.js | 152 +++++++++++++++++++++++++++ tsconfig.json | 52 +++++++++ 7 files changed, 449 insertions(+), 10 deletions(-) create mode 100644 src/types.js create mode 100644 tsconfig.json diff --git a/package.json b/package.json index 6f061b59..b2fbf581 100755 --- a/package.json +++ b/package.json @@ -4,9 +4,29 @@ "description": "An accessible map component", "main": "dist/umd/index.js", "module": "dist/esm/index.js", + "types": "./dist/src/index.d.ts", "exports": { - "import": "./dist/esm/index.js", - "require": "./dist/umd/index.js" + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/umd/index.js" + }, + "./plugins/*": { + "types": "./dist/plugins/*/src/index.d.ts", + "import": "./plugins/*/dist/esm/index.js" + }, + "./plugins/beta/*": { + "types": "./dist/plugins/beta/*/src/index.d.ts", + "import": "./plugins/beta/*/dist/esm/index.js" + }, + "./providers/*": { + "types": "./dist/providers/*/src/index.d.ts", + "import": "./providers/*/dist/esm/index.js" + }, + "./providers/beta/*": { + "types": "./dist/providers/beta/*/src/index.d.ts", + "import": "./providers/beta/*/dist/esm/index.js" + } }, "type": "module", "scripts": { @@ -17,7 +37,9 @@ "prod": "webpack serve --config webpack.prod.mjs", "build:umd": "webpack --config webpack.umd.mjs --mode production", "build:esm": "webpack --config webpack.esm.mjs --mode production", - "build": "npm run clean && npm-run-all --parallel build:umd build:esm", + "types": "tsc", + "types:watch": "tsc --watch", + "build": "npm run clean && npm-run-all --parallel build:umd build:esm && npm run types", "type-check": "tsc --noEmit", "lint": "npm run lint:js && npm run lint:scss", "lint:js": "standard \"src/**/*.{js,jsx,ts,tsx}\"", @@ -26,7 +48,8 @@ "lint:js:fix": "standard \"src/**/*.{js,jsx,ts,tsx}\" --fix", "lint:scss:fix": "stylelint \"src/**/*.{css,scss}\" --fix", "test": "jest --color --coverage --verbose", - "report": "webpack --profile --json --config webpack.prod.mjs > stats.json && webpack-bundle-analyzer --host 0.0.0.0 --port 8888 ./stats.json ./public" + "report": "webpack --profile --json --config webpack.prod.mjs > stats.json && webpack-bundle-analyzer --host 0.0.0.0 --port 8888 ./stats.json ./public", + "prepublishOnly": "npm run build" }, "standard": { "envs": [ diff --git a/src/InteractiveMap/InteractiveMap.js b/src/InteractiveMap/InteractiveMap.js index fb351d60..98e68862 100755 --- a/src/InteractiveMap/InteractiveMap.js +++ b/src/InteractiveMap/InteractiveMap.js @@ -1,5 +1,6 @@ // src/InteractiveMap/InteractiveMap.js import '../scss/main.scss' +import '../types.js' // Import type definitions import historyManager from './historyManager.js' import { parseDataProperties } from './parseDataProperties.js' import { checkDeviceSupport } from './deviceChecker.js' @@ -14,6 +15,24 @@ import { createReverseGeocode } from '../services/reverseGeocode.js' import { EVENTS as events } from '../config/events.js' import { createEventBus } from '../services/eventBus.js' +/** + * Main InteractiveMap class for creating accessible map components. + * + * @example + * ```js + * import InteractiveMap from '@defra/interactive-map' + * import { createMapLibreProvider } from '@defra/interactive-map/providers/maplibre' + * + * const map = new InteractiveMap('map-container', { + * behaviour: 'inline', + * center: [-1.5, 52.0], + * zoom: 10, + * mapProvider: createMapLibreProvider() + * }) + * + * map.loadApp() + * ``` + */ export default class InteractiveMap { _openButton = null _root = null // keep react root internally @@ -21,6 +40,11 @@ export default class InteractiveMap { _interfaceDetectorCleanup = null _hybridBehaviourCleanup = null + /** + * Create a new InteractiveMap instance. + * @param {string} id - DOM element ID to mount the map + * @param {import('../config/defaults.js').InteractiveMapConfig} [props] - Configuration options + */ constructor (id, props = {}) { this.id = id this.rootEl = document.getElementById(id) @@ -94,6 +118,10 @@ export default class InteractiveMap { } // Public methods + /** + * Load and render the map application. + * @returns {Promise} + */ async loadApp () { if (this._openButton) { this._openButton.style.display = 'none' @@ -146,6 +174,10 @@ export default class InteractiveMap { } } + /** + * Remove the app from DOM but keep instance. + * @returns {void} + */ removeApp () { if (this._root && typeof this.unmount === 'function') { this.unmount() @@ -162,6 +194,10 @@ export default class InteractiveMap { this.eventBus.emit(events.MAP_DESTROY, { mapId: this.id }) } + /** + * Fully destroy the map instance and clean up resources. + * @returns {void} + */ destroy () { this.removeApp() this._breakpointDetector?.destroy() @@ -172,53 +208,130 @@ export default class InteractiveMap { } // API - EventBus methods + /** + * Subscribe to a map event. + * @param {string} eventName - Event name (e.g., 'map:ready', 'map:click') + * @param {(...args: any[]) => void} handler - Event handler function + * @returns {void} + * @example + * ```js + * map.on('map:ready', () => console.log('Map is ready')) + * map.on('map:click', (event) => console.log('Clicked at', event.coordinates)) + * ``` + */ on (...args) { this.eventBus.on(...args) } + /** + * Unsubscribe from a map event. + * @param {string} eventName - Event name + * @param {(...args: any[]) => void} [handler] - Specific handler to remove (omit to remove all) + * @returns {void} + */ off (...args) { this.eventBus.off(...args) } + /** + * Emit a map event. + * @param {string} eventName - Event name + * @param {...any} args - Event arguments + * @returns {void} + */ emit (...args) { this.eventBus.emit(...args) } // API - location markers + /** + * Add a marker to the map. + * @param {string} id - Unique marker identifier + * @param {[number, number]} coords - Marker coordinates [lng, lat] + * @param {import('../types.js').MarkerOptions} [options] - Marker options + * @returns {void} + * @example + * ```js + * map.addMarker('location-1', [-1.5, 52.0], { color: '#0000ff' }) + * ``` + */ addMarker (id, coords, options) { this.eventBus.emit(events.APP_ADD_MARKER, { id, coords, options }) } + /** + * Remove a marker from the map. + * @param {string} id - Marker identifier to remove + * @returns {void} + */ removeMarker (id) { this.eventBus.emit(events.APP_REMOVE_MARKER, id) } // API - change app mode + /** + * Set the current interaction mode. + * @param {string} mode - Mode identifier + * @returns {void} + */ setMode (mode) { this.eventBus.emit(events.APP_SET_MODE, mode) } // Interface API add button/panel/control, remove panel + /** + * Add a button to the map UI. + * @param {string} id - Button identifier + * @param {import('../types.js').ButtonDefinition} config - Button configuration + * @returns {void} + */ addButton (id, config) { this.eventBus.emit(events.APP_ADD_BUTTON, { id, config }) } + /** + * Add a panel to the map UI. + * @param {string} id - Panel identifier + * @param {import('../types.js').PanelDefinition} config - Panel configuration + * @returns {void} + */ addPanel (id, config) { this.eventBus.emit(events.APP_ADD_PANEL, { id, config }) } + /** + * Remove a panel from the map UI. + * @param {string} id - Panel identifier + * @returns {void} + */ removePanel (id) { this.eventBus.emit(events.APP_REMOVE_PANEL, id) } + /** + * Show a panel. + * @param {string} id - Panel identifier + * @returns {void} + */ showPanel (id) { this.eventBus.emit(events.APP_SHOW_PANEL, id) } + /** + * Hide a panel. + * @param {string} id - Panel identifier + * @returns {void} + */ hidePanel (id) { this.eventBus.emit(events.APP_HIDE_PANEL, id) } + /** + * Add a control to the map UI. + * @param {string} id - Control identifier + * @param {import('../types.js').ControlDefinition} config - Control configuration + * @returns {void} + */ addControl (id, config) { this.eventBus.emit(events.APP_ADD_CONTROL, { id, config }) } diff --git a/src/config/defaults.js b/src/config/defaults.js index 697995c4..ad7a3029 100755 --- a/src/config/defaults.js +++ b/src/config/defaults.js @@ -1,3 +1,48 @@ +/** + * @typedef {Object} InteractiveMapConfig + * @property {string} [mapViewParamKey='mv'] - URL parameter key for map view state + * @property {'buttonFirst' | 'hybrid' | 'inline'} [behaviour='buttonFirst'] - How map integrates with page + * @property {number} [maxMobileWidth=640] - Max width (px) for mobile layout + * @property {number} [minDesktopWidth=835] - Min width (px) for desktop layout + * @property {number | null} [hybridWidth] - Width for hybrid behaviour switch + * @property {string} [containerHeight='600px'] - CSS height for map container + * @property {string} [backgroundColor] - CSS background color + * @property {'small' | 'medium' | 'large'} [mapSize='small'] - Preset map size + * @property {'light' | 'dark'} [appColorScheme='light'] - App color scheme + * @property {boolean} [autoColorScheme=false] - Auto-detect system color scheme + * @property {string} [mapLabel='Interactive map'] - Accessible label + * @property {string} [buttonText='Map view'] - Open button text + * @property {string} [buttonClass='im-c-open-map-button'] - Open button CSS class + * @property {string} [deviceNotSupportedText] - Unsupported device message + * @property {string} [genericErrorText] - Generic error message + * @property {string} [keyboardHintText] - Keyboard hint message + * @property {string} [pageTitle='Map view'] - Fullscreen page title + * @property {number} [zoomDelta=1] - Zoom button increment + * @property {number} [nudgeZoomDelta=0.1] - Keyboard zoom increment + * @property {number} [panDelta=100] - Keyboard pan distance (px) + * @property {number} [nudgePanDelta=5] - Fine pan distance (px) + * @property {boolean} [enableFullscreen=false] - Enable fullscreen mode + * @property {boolean} [enableZoomControls=false] - Show zoom controls + * @property {boolean} [hasExitButton] - Show exit button + * @property {boolean} [readMapText=true] - Enable screen reader text + * @property {[number, number]} [center] - Initial center [lng, lat] + * @property {number} [zoom] - Initial zoom level + * @property {[number, number, number, number]} [bounds] - Initial bounds + * @property {[number, number, number, number]} [extent] - Alias for bounds + * @property {number} [minZoom] - Minimum zoom level + * @property {number} [maxZoom] - Maximum zoom level + * @property {[number, number, number, number]} [maxExtent] - Maximum viewable extent + * @property {import('../types.js').MarkerConfig[]} [markers] - Initial markers + * @property {string} [markerShape='pin'] - Default marker shape + * @property {string} [markerColor='#ff0000'] - Default marker color + * @property {import('../types.js').MapProviderDescriptor} mapProvider - Map provider (required) + * @property {import('../types.js').ReverseGeocodeProviderDescriptor} [reverseGeocodeProvider] + * @property {import('../types.js').MapStyleConfig} [mapStyle] - Map style config + * @property {import('../types.js').PluginDescriptor[]} [plugins] - Plugins to load + * @property {import('../types.js').TransformRequestFn} [transformRequest] - Request transformer + */ + +/** @type {InteractiveMapConfig} */ const defaults = { mapViewParamKey: 'mv', behaviour: 'buttonFirst', diff --git a/src/config/events.js b/src/config/events.js index 3f99555f..4d1cfa79 100644 --- a/src/config/events.js +++ b/src/config/events.js @@ -1,39 +1,86 @@ -export const EVENTS = { - // App commands +/** + * Map event constants. + * Use these with `map.on()` and `map.off()` to subscribe to events. + * + * @example + * ```js + * import InteractiveMap, { EVENTS } from '@defra/interactive-map' + * + * map.on(EVENTS.MAP_READY, () => console.log('Ready!')) + * map.on(EVENTS.MAP_CLICK, (e) => console.log(e.coordinates)) + * ``` + */ +export const EVENTS = /** @type {const} */ ({ + // App commands (emit to trigger actions) + /** Emit to add a marker */ APP_ADD_MARKER: 'app:addmarker', + /** Emit to remove a marker */ APP_REMOVE_MARKER: 'app:removemarker', + /** Emit to set interaction mode */ APP_SET_MODE: 'app:setmode', + /** Emit to revert to previous mode */ APP_REVERT_MODE: 'app:revertmode', + /** Emit to add a button */ APP_ADD_BUTTON: 'app:addbutton', + /** Emit to add a panel */ APP_ADD_PANEL: 'app:addpanel', + /** Emit to remove a panel */ APP_REMOVE_PANEL: 'app:removepanel', + /** Emit to show a panel */ APP_SHOW_PANEL: 'app:showpanel', + /** Emit to hide a panel */ APP_HIDE_PANEL: 'app:hidepanel', + /** Emit to add a control */ APP_ADD_CONTROL: 'app:addcontrol', - // App responses + // App responses (subscribe to receive) + /** Fired when app is fully initialized */ APP_READY: 'app:ready', + /** Fired when a panel is opened */ APP_PANEL_OPENED: 'app:panelopened', + /** Fired when a panel is closed */ APP_PANEL_CLOSED: 'app:panelclosed', // Map commands + /** Emit to change map style */ MAP_SET_STYLE: 'map:setstyle', + /** Emit to change map size */ MAP_SET_SIZE: 'map:setsize', + /** Emit to change pixel ratio */ MAP_SET_PIXEL_RATIO: 'map:setpixelratio', // Map responses + /** Fired when map styles are initialized */ MAP_INIT_MAP_STYLES: 'map:initmapstyles', + /** Fired when map style changes */ MAP_STYLE_CHANGE: 'map:stylechange', + /** Fired when map is loaded */ MAP_LOADED: 'map:loaded', + /** Fired when map is ready for interaction */ MAP_READY: 'map:ready', + /** Fired on first idle after load */ MAP_FIRST_IDLE: 'map:firstidle', + /** Fired when map starts moving */ MAP_MOVE_START: 'map:movestart', + /** Fired during map movement */ MAP_MOVE: 'map:move', + /** Fired when map stops moving */ MAP_MOVE_END: 'map:moveend', + /** Fired when map state updates */ MAP_STATE_UPDATED: 'map:stateupdated', + /** Fired when map data changes */ MAP_DATA_CHANGE: 'map:datachange', + /** Fired on each render frame */ MAP_RENDER: 'map:render', + /** Fired on map click */ MAP_CLICK: 'map:click', + /** Fired when exiting map */ MAP_EXIT: 'map:exit', + /** Fired when map is destroyed */ MAP_DESTROY: 'map:destroy' -} +}) + +/** + * @typedef {typeof EVENTS} EventsType + * @typedef {EventsType[keyof EventsType]} EventName + */ diff --git a/src/index.js b/src/index.js index d055e5b3..f554d91f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,10 @@ -import InteractiveMap from './InteractiveMap/InteractiveMap.js' +// Re-export types for consumers +// This allows: import('@defra/interactive-map').PluginDescriptor +export * from './types.js' -export default InteractiveMap +// Export the main class +export { default } from './InteractiveMap/InteractiveMap.js' +export { default as InteractiveMap } from './InteractiveMap/InteractiveMap.js' + +// Export events +export { EVENTS } from './config/events.js' diff --git a/src/types.js b/src/types.js new file mode 100644 index 00000000..edfc6335 --- /dev/null +++ b/src/types.js @@ -0,0 +1,152 @@ +// src/types.js +// This file contains shared JSDoc type definitions used across the codebase. +// These are used by TypeScript to generate .d.ts files. + +/** + * @typedef {Object} MapStyleConfig + * @property {string} [id] - Unique identifier for the style + * @property {string} [label] - Display label for the style + * @property {string} url - URL to the style specification + * @property {string} [thumbnail] - URL to thumbnail image + * @property {string} [logo] - URL to logo image + * @property {string} [logoAltText] - Alt text for logo + * @property {string} [attribution] - Attribution text + * @property {string} [backgroundColor] - CSS background color + * @property {'light' | 'dark'} [mapColorScheme] - Map color scheme + * @property {'light' | 'dark'} [appColorScheme] - App UI color scheme + */ + +/** + * @typedef {Object} MarkerConfig + * @property {string} id - Unique marker identifier + * @property {[number, number]} coords - Coordinates [lng, lat] or [x, y] + * @property {string | Record} [color] - Marker color or color per style ID + */ + +/** + * @typedef {Object} MarkerOptions + * @property {string | Record} [color] - Marker color + * @property {string} [shape] - Marker shape (e.g., 'pin') + */ + +/** + * @typedef {Object} MapProviderDescriptor + * @property {() => { isSupported: boolean, error?: string }} checkDeviceCapabilities + * @property {() => Promise} load + */ + +/** + * @typedef {Object} MapProviderLoadResult + * @property {new (options: any) => MapProvider} MapProvider + * @property {MapProviderConfig} mapProviderConfig + * @property {any} [mapFramework] + */ + +/** + * @typedef {Object} MapProviderConfig + * @property {'EPSG:4326' | 'EPSG:27700'} crs - Coordinate reference system + */ + +/** + * @typedef {Object} MapProvider + * @property {any} map - Underlying map instance + * @property {{ supportedShortcuts: string[], supportsMapSizes: boolean }} capabilities + * @property {(config: any) => Promise} initMap + * @property {() => void} destroyMap + * @property {(options: { center?: [number, number], zoom?: number }) => void} setView + * @property {(delta: number) => void} zoomIn + * @property {(delta: number) => void} zoomOut + * @property {() => [number, number]} getCenter + * @property {() => number} getZoom + * @property {() => [number, number, number, number]} getBounds + */ + +/** + * @typedef {Object} ReverseGeocodeProviderDescriptor + * @property {string} [url] + * @property {TransformRequestFn} [transformRequest] + * @property {() => Promise} load + */ + +/** + * @typedef {(request: Request) => Request | Promise} TransformRequestFn + */ + +/** + * @typedef {(url: string, transformRequest: TransformRequestFn | undefined, crs: string, zoom: number, coord: [number, number]) => Promise} ReverseGeocodeFn + */ + +/** + * @typedef {Object} PluginDescriptor + * @property {string} id - Unique plugin identifier + * @property {() => Promise} load - Async loader + * @property {Partial} [manifest] - Optional manifest overrides + */ + +/** + * @typedef {Object} PluginManifest + * @property {import('preact').ComponentType} [InitComponent] + * @property {{ initialState: Record, actions: Record }} [reducer] + * @property {ButtonDefinition[]} [buttons] + * @property {PanelDefinition[]} [panels] + * @property {ControlDefinition[]} [controls] + * @property {Record} [api] + */ + +/** + * @typedef {Object} SlotConfig + * @property {string} slot - Slot identifier + * @property {boolean} [showLabel] + * @property {number} [order] + * @property {string} [width] + */ + +/** + * @typedef {Object} ButtonDefinition + * @property {string} id + * @property {string | (() => string)} label + * @property {string} [iconId] + * @property {string} [panelId] + * @property {string} [group] + * @property {SlotConfig} mobile + * @property {SlotConfig} tablet + * @property {SlotConfig} desktop + * @property {(context: any) => boolean} [excludeWhen] + * @property {(context: any) => boolean} [hiddenWhen] + * @property {(context: any) => boolean} [enableWhen] + * @property {(event: MouseEvent, context: any) => void} [onClick] + */ + +/** + * @typedef {Object} PanelSlotConfig + * @property {string} slot + * @property {boolean} [initiallyOpen] + * @property {boolean} [dismissable] + * @property {boolean} [modal] + * @property {boolean} [exclusive] + * @property {string} [width] + */ + +/** + * @typedef {Object} PanelDefinition + * @property {string} id + * @property {string} label + * @property {boolean} [showLabel] + * @property {import('preact').ComponentType} [render] + * @property {string} [html] + * @property {PanelSlotConfig} mobile + * @property {PanelSlotConfig} tablet + * @property {PanelSlotConfig} desktop + */ + +/** + * @typedef {Object} ControlDefinition + * @property {string} id + * @property {string} label + * @property {import('preact').ComponentType} render + * @property {{ slot: string }} mobile + * @property {{ slot: string }} tablet + * @property {{ slot: string }} desktop + */ + +export {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a5351cb9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,52 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": true, + "checkJs": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "declarationDir": "./dist", + "rootDir": ".", + "strict": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "noImplicitAny": false, + "noImplicitThis": false, + "strictNullChecks": false, + "strictFunctionTypes": false, + "strictBindCallApply": false, + "strictPropertyInitialization": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "paths": { + "react": ["./node_modules/preact/compat"], + "react-dom": ["./node_modules/preact/compat"] + } + }, + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "plugins/**/*.js", + "plugins/**/*.jsx", + "providers/**/*.js", + "providers/**/*.jsx" + ], + "exclude": [ + "node_modules", + "dist", + "coverage", + "demo", + "**/dist/**", + "**/*.test.js", + "**/*.test.jsx" + ] +} From f2db3b525cad8770c5cc07ecdea357d79e0570cc Mon Sep 17 00:00:00 2001 From: Alex Luckett Date: Thu, 29 Jan 2026 14:44:09 +0000 Subject: [PATCH 2/2] more jsdoc --- providers/maplibre/src/index.js | 5 ++- providers/maplibre/src/maplibreProvider.js | 11 +++++++ src/types.js | 36 +++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/providers/maplibre/src/index.js b/providers/maplibre/src/index.js index 6b5d47d2..d374afda 100755 --- a/providers/maplibre/src/index.js +++ b/providers/maplibre/src/index.js @@ -32,9 +32,12 @@ export default function (config = {}) { const MapProvider = (await import(/* webpackChunkName: "im-maplibre-provider" */ './maplibreProvider.js')).default + /** @type {"EPSG:4326" | "EPSG:27700"} */ + const crs = 'EPSG:4326' + const mapProviderConfig = { ...config, - crs: 'EPSG:4326' + crs } return { diff --git a/providers/maplibre/src/maplibreProvider.js b/providers/maplibre/src/maplibreProvider.js index 36529a3d..f01e7865 100755 --- a/providers/maplibre/src/maplibreProvider.js +++ b/providers/maplibre/src/maplibreProvider.js @@ -94,6 +94,11 @@ export default class MapLibreProvider { // Side-effects // ========================== + /** + * @param {Object} options + * @param {[number, number]=} options.center + * @param {number=} options.zoom + */ setView ({ center, zoom }) { this.map.flyTo({ center: center || this.getCenter(), @@ -157,6 +162,9 @@ export default class MapLibreProvider { // Read-only getters // ========================== + /** + * @returns {[number, number]} + */ getCenter () { const coord = this.map.getCenter() return [Number(coord.lng.toFixed(7)), Number(coord.lat.toFixed(7))] @@ -166,6 +174,9 @@ export default class MapLibreProvider { return Number(this.map.getZoom().toFixed(7)) } + /** + * @returns {[number, number, number, number]} + */ getBounds () { return this.map.getBounds().toArray().flat(1) } diff --git a/src/types.js b/src/types.js index edfc6335..21bf78f9 100644 --- a/src/types.js +++ b/src/types.js @@ -83,9 +83,43 @@ * @property {Partial} [manifest] - Optional manifest overrides */ +/** + * @typedef {Object} PluginComponentProps + * @property {Object} appConfig - Application configuration with all settings + * @property {Object} appState - Application state from AppProvider + * @property {string} appState.mode - Current mode + * @property {string} appState.breakpoint - Current breakpoint ('mobile' | 'tablet' | 'desktop') + * @property {string} appState.interfaceType - Interface type ('mouse' | 'touch' | 'keyboard') + * @property {boolean} appState.isFullscreen - Whether app is in fullscreen + * @property {boolean} appState.isLayoutReady - Whether layout is initialized + * @property {Object} appState.layoutRefs - React refs for layout containers + * @property {Object} mapState - Map state from MapProvider + * @property {boolean} mapState.isMapReady - Whether map is initialized + * @property {MapStyleConfig} [mapState.mapStyle] - Current map style configuration + * @property {string} [mapState.mapSize] - Current map size preset + * @property {[number, number]} [mapState.center] - Map center coordinates + * @property {number} [mapState.zoom] - Map zoom level + * @property {boolean} [mapState.isAtMaxZoom] - Whether at maximum zoom + * @property {boolean} [mapState.isAtMinZoom] - Whether at minimum zoom + * @property {Object} mapState.crossHair - Target marker state + * @property {Object} mapState.markers - Map markers state + * @property {Object} services - Core services + * @property {(message: string) => void} services.announce - Screen reader announcer + * @property {(zoom: number, center: [number, number]) => Promise} services.reverseGeocode - Reverse geocoding service + * @property {Object} services.events - Event constant definitions + * @property {Object} services.eventBus - Event bus with on/off/emit methods + * @property {() => void} services.closeApp - Function to close the app + * @property {import('react').MutableRefObject} services.mapStatusRef - Ref to map status element + * @property {Record} buttonConfig - Button configurations filtered for this plugin + * @property {MapProvider} mapProvider - Map provider instance with capabilities and map control methods + * @property {Object} pluginState - Plugin-specific state from PluginProvider + * @property {Function} pluginState.dispatch - Dispatch function for plugin state updates + * @property {Object} [pluginConfig] - Static plugin configuration from manifest + */ + /** * @typedef {Object} PluginManifest - * @property {import('preact').ComponentType} [InitComponent] + * @property {(props: PluginComponentProps) => any} [InitComponent] * @property {{ initialState: Record, actions: Record }} [reducer] * @property {ButtonDefinition[]} [buttons] * @property {PanelDefinition[]} [panels]